testcontainers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Integration Testing with TestContainers

使用TestContainers进行集成测试

When to Use This Skill

何时使用该技能

Use this skill when:
  • Writing integration tests that need real infrastructure (databases, caches, message queues)
  • Testing data access layers against actual databases
  • Verifying message queue integrations
  • Testing Redis caching behavior
  • Avoiding mocks for infrastructure components
  • Ensuring tests work against production-like environments
  • Testing database migrations and schema changes
适用于以下场景:
  • 编写需要真实基础设施(数据库、缓存、消息队列)的集成测试
  • 针对实际数据库测试数据访问层
  • 验证消息队列集成效果
  • 测试Redis缓存行为
  • 避免为基础设施组件编写mock
  • 确保测试在类生产环境下可正常运行
  • 测试数据库迁移和schema变更

Core Principles

核心原则

  1. Real Infrastructure Over Mocks - Use actual databases/services in containers, not mocks
  2. Test Isolation - Each test gets fresh containers or fresh data
  3. Automatic Cleanup - TestContainers handles container lifecycle and cleanup
  4. Fast Startup - Reuse containers across tests in the same class when appropriate
  5. CI/CD Compatible - Works seamlessly in Docker-enabled CI environments
  6. Port Randomization - Containers use random ports to avoid conflicts
  1. 优先使用真实基础设施而非Mock - 在容器中使用实际的数据库/服务,而非mock
  2. 测试隔离 - 每个测试都使用全新的容器或全新的数据
  3. 自动清理 - TestContainers负责处理容器的生命周期和清理工作
  4. 快速启动 - 合适的情况下可在同一个类的多个测试中复用容器
  5. 兼容CI/CD - 可在支持Docker的CI环境中无缝运行
  6. 端口随机化 - 容器使用随机端口避免冲突

Why TestContainers Over Mocks?

为什么选择TestContainers而非Mock?

Problems with Mocking Infrastructure

Mock基础设施的问题

csharp
// BAD: Mocking a database
public class OrderRepositoryTests
{
    private readonly Mock<IDbConnection> _mockDb = new();

    [Fact]
    public async Task GetOrder_ReturnsOrder()
    {
        // This doesn't test real SQL behavior, constraints, or performance
        _mockDb.Setup(db => db.QueryAsync<Order>(It.IsAny<string>()))
            .ReturnsAsync(new[] { new Order { Id = 1 } });

        var repo = new OrderRepository(_mockDb.Object);
        var order = await repo.GetOrderAsync(1);

        Assert.NotNull(order);
    }
}
Problems:
  • Doesn't test actual SQL queries
  • Misses database constraints, indexes, and performance
  • Can give false confidence
  • Doesn't catch SQL syntax errors or schema mismatches
csharp
// BAD: Mocking a database
public class OrderRepositoryTests
{
    private readonly Mock<IDbConnection> _mockDb = new();

    [Fact]
    public async Task GetOrder_ReturnsOrder()
    {
        // This doesn't test real SQL behavior, constraints, or performance
        _mockDb.Setup(db => db.QueryAsync<Order>(It.IsAny<string>()))
            .ReturnsAsync(new[] { new Order { Id = 1 } });

        var repo = new OrderRepository(_mockDb.Object);
        var order = await repo.GetOrderAsync(1);

        Assert.NotNull(order);
    }
}
存在的问题:
  • 无法测试实际的SQL查询
  • 无法发现数据库约束、索引和性能问题
  • 可能给出错误的测试信心
  • 无法捕获SQL语法错误或schema不匹配问题

Better: TestContainers with Real Database

更优方案:搭配真实数据库使用TestContainers

csharp
// GOOD: Testing against a real database
public class OrderRepositoryTests : IAsyncLifetime
{
    private readonly TestcontainersContainer _dbContainer;
    private IDbConnection _connection;

    public OrderRepositoryTests()
    {
        _dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithEnvironment("ACCEPT_EULA", "Y")
            .WithEnvironment("SA_PASSWORD", "Your_password123")
            .WithPortBinding(1433, true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();

        var port = _dbContainer.GetMappedPublicPort(1433);
        var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";

        _connection = new SqlConnection(connectionString);
        await _connection.OpenAsync();

        // Run migrations
        await RunMigrationsAsync(_connection);
    }

    public async Task DisposeAsync()
    {
        await _connection.DisposeAsync();
        await _dbContainer.DisposeAsync();
    }

    [Fact]
    public async Task GetOrder_WithRealDatabase_ReturnsOrder()
    {
        // Arrange: Insert real test data
        await _connection.ExecuteAsync(
            "INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");

        var repo = new OrderRepository(_connection);

        // Act: Execute against real database
        var order = await repo.GetOrderAsync(1);

        // Assert: Verify actual database behavior
        Assert.NotNull(order);
        Assert.Equal(1, order.Id);
        Assert.Equal("CUST1", order.CustomerId);
        Assert.Equal(100.00m, order.Total);
    }
}
Benefits:
  • Tests real SQL queries and database behavior
  • Catches constraint violations, index issues, and performance problems
  • Verifies migrations work correctly
  • Gives true confidence in data access layer
csharp
// GOOD: Testing against a real database
public class OrderRepositoryTests : IAsyncLifetime
{
    private readonly TestcontainersContainer _dbContainer;
    private IDbConnection _connection;

    public OrderRepositoryTests()
    {
        _dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithEnvironment("ACCEPT_EULA", "Y")
            .WithEnvironment("SA_PASSWORD", "Your_password123")
            .WithPortBinding(1433, true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();

        var port = _dbContainer.GetMappedPublicPort(1433);
        var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";

        _connection = new SqlConnection(connectionString);
        await _connection.OpenAsync();

        // Run migrations
        await RunMigrationsAsync(_connection);
    }

    public async Task DisposeAsync()
    {
        await _connection.DisposeAsync();
        await _dbContainer.DisposeAsync();
    }

    [Fact]
    public async Task GetOrder_WithRealDatabase_ReturnsOrder()
    {
        // Arrange: Insert real test data
        await _connection.ExecuteAsync(
            "INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");

        var repo = new OrderRepository(_connection);

        // Act: Execute against real database
        var order = await repo.GetOrderAsync(1);

        // Assert: Verify actual database behavior
        Assert.NotNull(order);
        Assert.Equal(1, order.Id);
        Assert.Equal("CUST1", order.CustomerId);
        Assert.Equal(100.00m, order.Total);
    }
}
优势:
  • 测试真实SQL查询和数据库行为
  • 可以捕获约束违反、索引问题和性能问题
  • 验证迁移是否正常工作
  • 让你对数据访问层有真正的信心

Required NuGet Packages

所需NuGet包

xml
<ItemGroup>
  <PackageReference Include="Testcontainers" Version="*" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />

  <!-- Database-specific packages -->
  <PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
  <PackageReference Include="Npgsql" Version="*" /> <!-- For PostgreSQL -->
  <PackageReference Include="MySqlConnector" Version="*" /> <!-- For MySQL -->

  <!-- Other infrastructure -->
  <PackageReference Include="StackExchange.Redis" Version="*" /> <!-- For Redis -->
  <PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- For RabbitMQ -->
</ItemGroup>
xml
<ItemGroup>
  <PackageReference Include="Testcontainers" Version="*" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />

  <!-- Database-specific packages -->
  <PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
  <PackageReference Include="Npgsql" Version="*" /> <!-- For PostgreSQL -->
  <PackageReference Include="MySqlConnector" Version="*" /> <!-- For MySQL -->

  <!-- Other infrastructure -->
  <PackageReference Include="StackExchange.Redis" Version="*" /> <!-- For Redis -->
  <PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- For RabbitMQ -->
</ItemGroup>

Getting Started

入门指南

The Testcontainers library provides a simple API for managing Docker containers in your tests. Each test can spin up the infrastructure it needs, and Testcontainers handles the lifecycle automatically.
Testcontainers库提供了简单的API,用于在测试中管理Docker容器。每个测试都可以启动它需要的基础设施,Testcontainers会自动处理生命周期。

Reference Documentation

参考文档

For detailed patterns and examples, see the reference files:
  • Database Containers - SQL Server, PostgreSQL, MySQL, and migration patterns
  • Message Broker Containers - RabbitMQ, Kafka, and Service Bus patterns
  • Advanced Patterns - Networks, volumes, wait strategies, cleanup, and performance optimization
有关详细模式和示例,请参阅参考文件:
  • 数据库容器 - SQL Server、PostgreSQL、MySQL和迁移模式
  • 消息代理容器 - RabbitMQ、Kafka和服务总线模式
  • 高级模式 - 网络、卷、等待策略、清理和性能优化

Best Practices

最佳实践

  1. Always Use IAsyncLifetime - Proper async setup and teardown
  2. Wait for Port Availability - Use
    WaitStrategy
    to ensure containers are ready
  3. Use Random Ports - Let TestContainers assign ports automatically
  4. Clean Data Between Tests - Either use fresh containers or truncate tables
  5. Reuse Containers When Possible - Faster than creating new ones for each test
  6. Test Real Queries - Don't just test mocks; verify actual SQL behavior
  7. Verify Constraints - Test foreign keys, unique constraints, indexes
  8. Test Transactions - Verify rollback and commit behavior
  9. Use Realistic Data - Test with production-like data volumes
  10. Handle Cleanup - Always dispose containers in
    DisposeAsync
  1. 始终使用IAsyncLifetime - 正确的异步设置和销毁逻辑
  2. 等待端口可用 - 使用
    WaitStrategy
    确保容器已准备就绪
  3. 使用随机端口 - 让TestContainers自动分配端口
  4. 测试之间清理数据 - 要么使用全新容器,要么截断数据表
  5. 尽可能复用容器 - 比为每个测试创建新容器速度更快
  6. 测试真实查询 - 不要只测试mock;验证实际SQL执行行为
  7. 验证约束 - 测试外键、唯一约束、索引逻辑
  8. 测试事务 - 验证回滚和提交行为
  9. 使用真实数据 - 使用类生产的数据量进行测试
  10. 做好清理工作 - 始终在
    DisposeAsync
    中销毁容器