| name | testcontainers |
| description | Guidance for Testcontainers integration testing library for .NET. USE FOR: spinning up real databases in Docker for integration tests, testing against PostgreSQL/SQL Server/Redis/RabbitMQ containers, verifying EF Core migrations against a real database, testing message broker consumers, replacing in-memory test doubles with real infrastructure in CI/CD pipelines. DO NOT USE FOR: unit testing (use Moq/AutoFixture), production container orchestration (use Kubernetes), load testing, or scenarios where Docker is unavailable.
|
| license | MIT |
| metadata | {"displayName":"Testcontainers","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"Testcontainers for .NET Documentation","url":"https://dotnet.testcontainers.org/"},{"title":"Testcontainers .NET GitHub Repository","url":"https://github.com/testcontainers/testcontainers-dotnet"},{"title":"Testcontainers NuGet Package","url":"https://www.nuget.org/packages/Testcontainers"}] |
Testcontainers
Overview
Testcontainers for .NET provides programmatic control over Docker containers for integration testing. It creates throwaway containers (databases, message brokers, caches) that spin up before tests and tear down afterward, giving each test suite a clean, isolated infrastructure instance. Testcontainers supports pre-built modules for PostgreSQL, SQL Server, MySQL, MongoDB, Redis, RabbitMQ, Kafka, Elasticsearch, and more. It works with xUnit, NUnit, and MSTest, and runs in CI/CD environments that have Docker available (including GitHub Actions, Azure DevOps, and GitLab CI).
PostgreSQL Integration Test
Spin up a PostgreSQL container and test against it with EF Core.
using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;
using Xunit;
public class PostgresTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres =
new PostgreSqlBuilder()
.WithDatabase("testdb")
.WithUsername("testuser")
.WithPassword("testpass")
.WithImage("postgres:16-alpine")
.Build();
public async Task InitializeAsync()
{
await _postgres.StartAsync();
}
[Fact]
public async Task Can_Insert_And_Query_Users()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_postgres.GetConnectionString())
.Options;
await using var context = new AppDbContext(options);
await context.Database.EnsureCreatedAsync();
context.Users.Add(new User
{
Name = "Alice",
Email = "alice@example.com"
});
await context.SaveChangesAsync();
var user = await context.Users
.FirstOrDefaultAsync(u => u.Email == "alice@example.com");
Assert.NotNull(user);
Assert.Equal(, user.Name);
}
[]
{
options = DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_postgres.GetConnectionString())
.Options;
context = AppDbContext(options);
context.Database.MigrateAsync();
canConnect = context.Database.CanConnectAsync();
Assert.True(canConnect);
}
{
_postgres.DisposeAsync();
}
}
SQL Server Integration Test
Use the MSSQL module for SQL Server testing.
using Microsoft.EntityFrameworkCore;
using Testcontainers.MsSql;
using Xunit;
public class SqlServerTests : IAsyncLifetime
{
private readonly MsSqlContainer _mssql =
new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithPassword("YourStrong!Passw0rd")
.Build();
public async Task InitializeAsync()
{
await _mssql.StartAsync();
}
[Fact]
public async Task Can_Execute_Stored_Procedure()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(_mssql.GetConnectionString())
.Options;
await using var context = new AppDbContext(options);
await context.Database.EnsureCreatedAsync();
context.Users.Add(new User
{
Name = "Bob",
Email = "bob@example.com"
});
await context.SaveChangesAsync();
var count = await context.Users.CountAsync();
Assert.Equal(1, count);
}
public async Task DisposeAsync()
{
await _mssql.DisposeAsync();
}
}
Redis Integration Test
Test caching operations against a real Redis container.
using StackExchange.Redis;
using Testcontainers.Redis;
using Xunit;
public class RedisTests : IAsyncLifetime
{
private readonly RedisContainer _redis =
new RedisBuilder()
.WithImage("redis:7-alpine")
.Build();
public async Task InitializeAsync()
{
await _redis.StartAsync();
}
[Fact]
public async Task Can_Set_And_Get_Cache_Values()
{
var connection = await ConnectionMultiplexer
.ConnectAsync(_redis.GetConnectionString());
var db = connection.GetDatabase();
await db.StringSetAsync("user:1:name", "Alice",
TimeSpan.FromMinutes(5));
string? value = await db.StringGetAsync("user:1:name");
Assert.Equal("Alice", value);
}
[Fact]
public async Task Cache_Expiration_Works()
{
var connection = await ConnectionMultiplexer
.ConnectAsync(_redis.GetConnectionString());
var db = connection.GetDatabase();
db.StringSetAsync(, ,
TimeSpan.FromMilliseconds());
Assert.True( db.KeyExistsAsync());
Task.Delay();
Assert.False( db.KeyExistsAsync());
}
{
_redis.DisposeAsync();
}
}
Shared Container with xUnit Collection Fixture
Share a single container across all tests in a collection for faster execution.
using Testcontainers.PostgreSql;
using Xunit;
public class DatabaseFixture : IAsyncLifetime
{
public PostgreSqlContainer Container { get; } =
new PostgreSqlBuilder()
.WithDatabase("sharedtestdb")
.WithUsername("testuser")
.WithPassword("testpass")
.Build();
public string ConnectionString =>
Container.GetConnectionString();
public async Task InitializeAsync()
{
await Container.StartAsync();
}
public async Task DisposeAsync()
{
await Container.DisposeAsync();
}
}
[CollectionDefinition("Database")]
public class DatabaseCollection
: ICollectionFixture<DatabaseFixture> { }
[Collection("Database")]
public class UserRepositoryTests
{
private readonly DatabaseFixture _db;
public UserRepositoryTests(DatabaseFixture db)
{
_db = db;
}
[Fact]
{
options = DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_db.ConnectionString)
.Options;
context = AppDbContext(options);
context.Database.EnsureCreatedAsync();
context.Users.Add( User
{
Name = ,
Email =
});
context.SaveChangesAsync();
count = context.Users.CountAsync();
Assert.True(count >= );
}
}
[]
{
DatabaseFixture _db;
{
_db = db;
}
[]
{
options = DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_db.ConnectionString)
.Options;
context = AppDbContext(options);
Assert.True( context.Database.CanConnectAsync());
}
}
WebApplicationFactory with Testcontainers
Replace the test database in ASP.NET Core integration tests.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.PostgreSql;
using Xunit;
public class ApiIntegrationTests
: IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public ApiIntegrationTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUsers_Returns_Ok()
{
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
}
}
public class CustomWebApplicationFactory
: WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres =
new PostgreSqlBuilder()
.WithDatabase("integrationtestdb")
.WithUsername("test")
.WithPassword("test")
.Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
descriptor = services.SingleOrDefault(d =>
d.ServiceType == (DbContextOptions<AppDbContext>));
(descriptor != )
services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(_postgres.GetConnectionString()));
});
}
{
_postgres.StartAsync();
}
{
_postgres.DisposeAsync();
.DisposeAsync();
}
}
Container Module Comparison
| Module | Image | Default Port | Use Case |
|---|
| PostgreSql | postgres:16-alpine | 5432 | Relational data, EF Core |
| MsSql | mssql/server:2022 | 1433 | SQL Server specific features |
| MySql | mysql:8 | 3306 | MySQL/MariaDB workloads |
| MongoDB | mongo:7 | 27017 | Document store testing |
| Redis | redis:7-alpine | 6379 | Caching, pub/sub |
| RabbitMQ | rabbitmq:3-management | 5672 | Message broker testing |
| Kafka | confluentinc/cp-kafka | 9092 | Event streaming |
| Elasticsearch | elasticsearch:8 | 9200 | Full-text search |
Best Practices
- Implement
IAsyncLifetime for container lifecycle management: start containers in InitializeAsync and dispose them in DisposeAsync to ensure clean setup and teardown for every test class.
- Use xUnit collection fixtures to share containers across test classes: avoid the overhead of starting a new container for each test class by defining
ICollectionFixture<T> with a shared container.
- Pin container image versions explicitly: use
postgres:16-alpine instead of postgres:latest to ensure reproducible builds; unpinned tags can introduce flaky tests when images update.
- Use
EnsureCreatedAsync or MigrateAsync to set up the schema: call one of these methods after the container starts to create tables before running test queries.
- Clean up data between tests when sharing containers: use
DELETE FROM or TRUNCATE in a [BeforeEach] hook to reset data state rather than spinning up a new container per test.
- Configure appropriate container startup timeouts: set
WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432)) to avoid tests failing because the container was not ready.
- Replace the real database in
WebApplicationFactory: override ConfigureWebHost to swap the production DbContext registration with one pointing to the Testcontainers connection string.
- Use lightweight Alpine-based images where available:
postgres:16-alpine pulls faster and uses less disk space than the full postgres:16 image, improving CI pipeline speed.
- Ensure Docker is available in your CI/CD environment: configure your CI runner with Docker support (Docker-in-Docker, privileged containers, or a Docker socket mount) before running Testcontainers tests.
- Test real database behavior, not ORM abstractions: use Testcontainers to test raw SQL queries, stored procedures, database constraints, and migration scripts that cannot be verified with in-memory providers.