Integration Testing with 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
Core Principles
- Real Infrastructure Over Mocks - Use actual databases/services in containers, not mocks
- Test Isolation - Each test gets fresh containers or fresh data
- Automatic Cleanup - TestContainers handles container lifecycle and cleanup
- Fast Startup - Reuse containers across tests in the same class when appropriate
- CI/CD Compatible - Works seamlessly in Docker-enabled CI environments
- Port Randomization - Containers use random ports to avoid conflicts
Why TestContainers Over Mocks?
Problems with Mocking Infrastructure
// 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
Better: TestContainers with Real Database
// 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
Required NuGet Packages
<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.
Reference Documentation
For detailed patterns and examples, see the reference files:
Best Practices
- Always Use IAsyncLifetime - Proper async setup and teardown
- Wait for Port Availability - Use
WaitStrategy to ensure containers are ready
- Use Random Ports - Let TestContainers assign ports automatically
- Clean Data Between Tests - Either use fresh containers or truncate tables
- Reuse Containers When Possible - Faster than creating new ones for each test
- Test Real Queries - Don't just test mocks; verify actual SQL behavior
- Verify Constraints - Test foreign keys, unique constraints, indexes
- Test Transactions - Verify rollback and commit behavior
- Use Realistic Data - Test with production-like data volumes
- Handle Cleanup - Always dispose containers in
DisposeAsync
1---2name: testcontainers3description: Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.4---5
6# Integration Testing with TestContainers
7
8## When to Use This Skill
9
10Use this skill when:
11- Writing integration tests that need real infrastructure (databases, caches, message queues)
12- Testing data access layers against actual databases
13- Verifying message queue integrations
14- Testing Redis caching behavior
15- Avoiding mocks for infrastructure components
16- Ensuring tests work against production-like environments
17- Testing database migrations and schema changes
18
19## Core Principles
20
211. **Real Infrastructure Over Mocks** - Use actual databases/services in containers, not mocks
222. **Test Isolation** - Each test gets fresh containers or fresh data
233. **Automatic Cleanup** - TestContainers handles container lifecycle and cleanup
244. **Fast Startup** - Reuse containers across tests in the same class when appropriate
255. **CI/CD Compatible** - Works seamlessly in Docker-enabled CI environments
266. **Port Randomization** - Containers use random ports to avoid conflicts
27
28## Why TestContainers Over Mocks?
29
30### Problems with Mocking Infrastructure
31
32```csharp
33// BAD: Mocking a database
34public 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
53Problems:
54- Doesn't test actual SQL queries
55- Misses database constraints, indexes, and performance
56- Can give false confidence
57- Doesn't catch SQL syntax errors or schema mismatches
58
59### Better: TestContainers with Real Database
60
61```csharp
62// GOOD: Testing against a real database
63public class OrderRepositoryTests : IAsyncLifetime
64{
65 private readonly TestcontainersContainer _dbContainer;
66 private IDbConnection _connection;
67
68 public OrderRepositoryTests()
69 {
70 _dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
71 .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
72 .WithEnvironment("ACCEPT_EULA", "Y")
73 .WithEnvironment("SA_PASSWORD", "Your_password123")
74 .WithPortBinding(1433, true)
75 .Build();
76 }
77
78 public async Task InitializeAsync()
79 {
80 await _dbContainer.StartAsync();
81
82 var port = _dbContainer.GetMappedPublicPort(1433);
83 var connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";
84
85 _connection = new SqlConnection(connectionString);
86 await _connection.OpenAsync();
87
88 // Run migrations
89 await RunMigrationsAsync(_connection);
90 }
91
92 public async Task DisposeAsync()
93 {
94 await _connection.DisposeAsync();
95 await _dbContainer.DisposeAsync();
96 }
97
98 [Fact]
99 public async Task GetOrder_WithRealDatabase_ReturnsOrder()
100 {
101 // Arrange: Insert real test data
102 await _connection.ExecuteAsync(
103 "INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");
104
105 var repo = new OrderRepository(_connection);
106
107 // Act: Execute against real database
108 var order = await repo.GetOrderAsync(1);
109
110 // Assert: Verify actual database behavior
111 Assert.NotNull(order);
112 Assert.Equal(1, order.Id);
113 Assert.Equal("CUST1", order.CustomerId);
114 Assert.Equal(100.00m, order.Total);
115 }
116}
117```
118
119Benefits:
120- Tests real SQL queries and database behavior
121- Catches constraint violations, index issues, and performance problems
122- Verifies migrations work correctly
123- Gives true confidence in data access layer
124
125## Required NuGet Packages
126
127```xml
128<ItemGroup>
129 <PackageReference Include="Testcontainers" Version="*" />
130 <PackageReference Include="xunit" Version="*" />
131 <PackageReference Include="xunit.runner.visualstudio" Version="*" />
132
133 <!-- Database-specific packages -->
134 <PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
135 <PackageReference Include="Npgsql" Version="*" /> <!-- For PostgreSQL -->
136 <PackageReference Include="MySqlConnector" Version="*" /> <!-- For MySQL -->
137
138 <!-- Other infrastructure -->
139 <PackageReference Include="StackExchange.Redis" Version="*" /> <!-- For Redis -->
140 <PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- For RabbitMQ -->
141</ItemGroup>
142```
143
144## Getting Started
145
146The 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.
147
148## Reference Documentation
149
150For detailed patterns and examples, see the reference files:
151
152- **[Database Containers](./reference/database-containers.md)** - SQL Server, PostgreSQL, MySQL, and migration patterns
153- **[Message Broker Containers](./reference/message-broker-containers.md)** - RabbitMQ, Kafka, and Service Bus patterns
154- **[Advanced Patterns](./reference/advanced-patterns.md)** - Networks, volumes, wait strategies, cleanup, and performance optimization
155
156## Best Practices
157
1581. **Always Use IAsyncLifetime** - Proper async setup and teardown
1592. **Wait for Port Availability** - Use `WaitStrategy` to ensure containers are ready
1603. **Use Random Ports** - Let TestContainers assign ports automatically
1614. **Clean Data Between Tests** - Either use fresh containers or truncate tables
1625. **Reuse Containers When Possible** - Faster than creating new ones for each test
1636. **Test Real Queries** - Don't just test mocks; verify actual SQL behavior
1647. **Verify Constraints** - Test foreign keys, unique constraints, indexes
1658. **Test Transactions** - Verify rollback and commit behavior
1669. **Use Realistic Data** - Test with production-like data volumes
16710. **Handle Cleanup** - Always dispose containers in `DisposeAsync`