$npx -y skills add Aaronontheweb/dotnet-skills --skill testcontainersWrite integration tests using TestContainers for .NET with xUnit. Covers infrastructure testing with real databases, message queues, and caches in Docker containers instead of mocks.
| 1 | # Integration Testing with TestContainers |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Writing integration tests that need real infrastructure (databases, caches, message queues) |
| 7 | - Testing data access layers against actual databases |
| 8 | - Verifying message queue integrations |
| 9 | - Testing Redis caching behavior |
| 10 | - Avoiding mocks for infrastructure components |
| 11 | - Ensuring tests work against production-like environments |
| 12 | - Testing database migrations and schema changes |
| 13 | |
| 14 | ## Reference Files |
| 15 | |
| 16 | - [database-patterns.md](database-patterns.md): SQL Server, PostgreSQL, and migration testing examples |
| 17 | - [infrastructure-patterns.md](infrastructure-patterns.md): Redis, RabbitMQ, multi-container networks, container reuse, and Respawn |
| 18 | |
| 19 | ## Core Principles |
| 20 | |
| 21 | 1. **Real Infrastructure Over Mocks** - Use actual databases/services in containers, not mocks |
| 22 | 2. **Test Isolation** - Each test gets fresh containers or fresh data |
| 23 | 3. **Automatic Cleanup** - TestContainers handles container lifecycle and cleanup |
| 24 | 4. **Fast Startup** - Reuse containers across tests in the same class when appropriate |
| 25 | 5. **CI/CD Compatible** - Works seamlessly in Docker-enabled CI environments |
| 26 | 6. **Port Randomization** - Containers use random ports to avoid conflicts |
| 27 | |
| 28 | ## Why TestContainers Over Mocks? |
| 29 | |
| 30 | ### The Problem with Mocking Infrastructure |
| 31 | |
| 32 | ```csharp |
| 33 | // BAD: Mocking a database |
| 34 | public class OrderRepositoryTests |
| 35 | { |
| 36 | private readonly Mock<IDbConnection> _mockDb = new(); |
| 37 | |
| 38 | [Fact] |
| 39 | public async Task GetOrder_ReturnsOrder() |
| 40 | { |
| 41 | // This doesn't test real SQL behavior, constraints, or performance |
| 42 | _mockDb.Setup(db => db.QueryAsync<Order>(It.IsAny<string>())) |
| 43 | .ReturnsAsync(new[] { new Order { Id = 1 } }); |
| 44 | |
| 45 | var repo = new OrderRepository(_mockDb.Object); |
| 46 | var order = await repo.GetOrderAsync(1); |
| 47 | |
| 48 | Assert.NotNull(order); |
| 49 | } |
| 50 | } |
| 51 | ``` |
| 52 | |
| 53 | Problems: doesn't test actual SQL queries, misses constraints/indexes, gives false confidence, doesn't catch SQL syntax errors. |
| 54 | |
| 55 | ### Better: TestContainers with Real Database |
| 56 | |
| 57 | ```csharp |
| 58 | // GOOD: Testing against a real database |
| 59 | public class OrderRepositoryTests : IAsyncLifetime |
| 60 | { |
| 61 | private readonly TestcontainersContainer _dbContainer; |
| 62 | private IDbConnection _connection; |
| 63 | |
| 64 | public OrderRepositoryTests() |
| 65 | { |
| 66 | _dbContainer = new TestcontainersBuilder<TestcontainersContainer>() |
| 67 | .WithImage("mcr.microsoft.com/mssql/server:2022-latest") |
| 68 | .WithEnvironment("ACCEPT_EULA", "Y") |
| 69 | .WithEnvironment("SA_PASSWORD", "Your_password123") |
| 70 | .WithPortBinding(1433, true) |
| 71 | .Build(); |
| 72 | } |
| 73 | |
| 74 | public async Task InitializeAsync() |
| 75 | { |
| 76 | await _dbContainer.StartAsync(); |
| 77 | var port = _dbContainer.GetMappedPublicPort(1433); |
| 78 | var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true"; |
| 79 | _connection = new SqlConnection(connectionString); |
| 80 | await _connection.OpenAsync(); |
| 81 | await RunMigrationsAsync(_connection); |
| 82 | } |
| 83 | |
| 84 | public async Task DisposeAsync() |
| 85 | { |
| 86 | await _connection.DisposeAsync(); |
| 87 | await _dbContainer.DisposeAsync(); |
| 88 | } |
| 89 | |
| 90 | [Fact] |
| 91 | public async Task GetOrder_WithRealDatabase_ReturnsOrder() |
| 92 | { |
| 93 | await _connection.ExecuteAsync( |
| 94 | "INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)"); |
| 95 | |
| 96 | var repo = new OrderRepository(_connection); |
| 97 | var order = await repo.GetOrderAsync(1); |
| 98 | |
| 99 | Assert.NotNull(order); |
| 100 | Assert.Equal("CUST1", order.CustomerId); |
| 101 | Assert.Equal(100.00m, order.Total); |
| 102 | } |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | See [database-patterns.md](database-patterns.md) for complete SQL Server, PostgreSQL, and migration testing examples. |
| 107 | |
| 108 | See [infrastructure-patterns.md](infrastructure-patterns.md) for Redis, RabbitMQ, multi-container networks, container reuse, and Respawn database reset patterns. |
| 109 | |
| 110 | ## Required NuGet Packages |
| 111 | |
| 112 | ```xml |
| 113 | <ItemGroup> |
| 114 | <PackageReference Include="Testcontainers" Version="*" /> |
| 115 | <PackageReference Include="xunit" Version="*" /> |
| 116 | <PackageReference Include="xunit.runner.visualstudio" Version="*" /> |
| 117 | |
| 118 | <!-- Database-specific packages --> |
| 119 | <PackageReference Include="Microsoft.Data.SqlClient" Version="*" /> |
| 120 | <PackageReference Include="Npgsql" Version="*" /> <!-- For PostgreSQL --> |
| 121 | <PackageReference Include="MySqlConnector" Version="*" /> <!-- For MySQL --> |
| 122 | |
| 123 | <!-- Other infrastructure --> |
| 124 | <PackageReference Include="StackExchange.Redis" Version="*" /> <!-- For Redis --> |
| 125 | <PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- For RabbitMQ --> |
| 126 | </ItemGroup> |
| 127 | ``` |
| 128 | |
| 129 | ## Best Practices |
| 130 | |
| 131 | 1. **Always Use IAsyncLifetime** - Proper async setup and teardown |
| 132 | 2. **Wait for Port Availability |