
Testcontainers Integration Tests
- 400 installs
- 1.1k repo stars
- Updated July 3, 2026
- aaronontheweb/dotnet-skills
testcontainers-integration-tests is a Claude agent skill that runs .NET integration tests with Testcontainers to spin up real databases, queues, and services in disposable containers for developers who need faithful pre-
About
testcontainers-integration-tests is an agent skill from aaronontheweb/dotnet-skills that guides Claude to write and run .NET integration tests using Testcontainers. Developers use it to start real databases, message queues, and dependent services in ephemeral Docker containers during test runs instead of mocks or shared staging environments. The skill targets faithful pre-release validation for .NET APIs and services. Reach for it when xUnit or NUnit suites need isolated, reproducible integration coverage against actual infrastructure components.
- Disposable real dependencies
- Docker-backed test environments
- Database and broker coverage
- Higher-fidelity integration suites
- CI-ready container lifecycle
Testcontainers Integration Tests by the numbers
- 400 all-time installs (skills.sh)
- Ranked #651 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill testcontainers-integration-testsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 400 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | July 3, 2026 |
| Repository | aaronontheweb/dotnet-skills ↗ |
How do you run .NET integration tests with Testcontainers?
Run .NET integration tests with Testcontainers to spin up real databases, queues, and services in disposable containers for faithful pre-release validation.
Who is it for?
.NET developers writing integration tests who need real databases, queues, and services in ephemeral Docker containers instead of mocks.
Skip if: Teams running unit-only tests, Java or Node Testcontainers projects, or production Docker deployment configuration without test scope.
When should I use this skill?
A .NET task mentions Testcontainers, integration tests, disposable database containers, or queue/service dependencies in xUnit or NUnit suites.
What you get
.NET integration test suites, Testcontainers fixtures, and disposable database or queue instances for CI validation.
- Integration test suites
- Testcontainers fixtures
- Disposable service containers
Files
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
Reference Files
- database-patterns.md: SQL Server, PostgreSQL, and migration testing examples
- infrastructure-patterns.md: Redis, RabbitMQ, multi-container networks, container reuse, and Respawn
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
Why TestContainers Over Mocks?
The Problem 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 constraints/indexes, gives false confidence, doesn't catch SQL syntax errors.
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();
await RunMigrationsAsync(_connection);
}
public async Task DisposeAsync()
{
await _connection.DisposeAsync();
await _dbContainer.DisposeAsync();
}
[Fact]
public async Task GetOrder_WithRealDatabase_ReturnsOrder()
{
await _connection.ExecuteAsync(
"INSERT INTO Orders (Id, CustomerId, Total) VALUES (1, 'CUST1', 100.00)");
var repo = new OrderRepository(_connection);
var order = await repo.GetOrderAsync(1);
Assert.NotNull(order);
Assert.Equal("CUST1", order.CustomerId);
Assert.Equal(100.00m, order.Total);
}
}See database-patterns.md for complete SQL Server, PostgreSQL, and migration testing examples.
See infrastructure-patterns.md for Redis, RabbitMQ, multi-container networks, container reuse, and Respawn database reset patterns.
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>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
Common Issues and Solutions
Container Startup Timeout
_container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:latest")
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(5432)
.WithTimeout(TimeSpan.FromMinutes(2)))
.Build();Port Already in Use
Always use random port mapping:
.WithPortBinding(5432, true) // true = assign random public portContainers Not Cleaning Up
Ensure proper disposal:
public async Task DisposeAsync()
{
await _connection?.DisposeAsync();
await _container?.DisposeAsync();
}Tests Fail in CI But Pass Locally
Ensure CI has Docker support:
# GitHub Actions
runs-on: ubuntu-latest # Has Docker pre-installedCI/CD Integration
GitHub Actions
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Run Integration Tests
run: |
dotnet test tests/YourApp.IntegrationTests \
--filter Category=Integration \
--logger trx
- name: Cleanup Containers
if: always()
run: docker container prune -fPerformance Tips
1. Reuse containers - Share fixtures across tests in a collection 2. Use Respawn - Reset data without recreating containers 3. Parallel execution - TestContainers handles port conflicts automatically 4. Use lightweight images - Alpine versions are smaller and faster 5. Cache images - Docker will cache pulled images locally
Database Testing Patterns
Full code examples for testing with SQL Server, PostgreSQL, and database migrations using TestContainers.
Contents
SQL Server Integration Tests
using Testcontainers;
using Xunit;
public class SqlServerTests : IAsyncLifetime
{
private readonly TestcontainersContainer _dbContainer;
private IDbConnection _db;
public SqlServerTests()
{
_dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SA_PASSWORD", "Your_password123")
.WithPortBinding(1433, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433))
.Build();
}
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
var port = _dbContainer.GetMappedPublicPort(1433);
var connectionString = $"Server=localhost,{port};Database=master;User Id=sa;Password=Your_password123;TrustServerCertificate=true";
_db = new SqlConnection(connectionString);
await _db.OpenAsync();
// Create test database
await _db.ExecuteAsync("CREATE DATABASE TestDb");
await _db.ExecuteAsync("USE TestDb");
// Run schema migrations
await _db.ExecuteAsync(@"
CREATE TABLE Orders (
Id INT PRIMARY KEY,
CustomerId NVARCHAR(50) NOT NULL,
Total DECIMAL(18,2) NOT NULL,
CreatedAt DATETIME2 DEFAULT GETUTCDATE()
)");
}
public async Task DisposeAsync()
{
await _db.DisposeAsync();
await _dbContainer.DisposeAsync();
}
[Fact]
public async Task CanInsertAndRetrieveOrder()
{
// Arrange
await _db.ExecuteAsync(@"
INSERT INTO Orders (Id, CustomerId, Total)
VALUES (1, 'CUST001', 99.99)");
// Act
var order = await _db.QuerySingleAsync<Order>(
"SELECT * FROM Orders WHERE Id = @Id",
new { Id = 1 });
// Assert
Assert.Equal(1, order.Id);
Assert.Equal("CUST001", order.CustomerId);
Assert.Equal(99.99m, order.Total);
}
}PostgreSQL Integration Tests
public class PostgreSqlTests : IAsyncLifetime
{
private readonly TestcontainersContainer _dbContainer;
private NpgsqlConnection _connection;
public PostgreSqlTests()
{
_dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:latest")
.WithEnvironment("POSTGRES_PASSWORD", "postgres")
.WithEnvironment("POSTGRES_DB", "testdb")
.WithPortBinding(5432, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
.Build();
}
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
var port = _dbContainer.GetMappedPublicPort(5432);
var connectionString = $"Host=localhost;Port={port};Database=testdb;Username=postgres;Password=postgres";
_connection = new NpgsqlConnection(connectionString);
await _connection.OpenAsync();
// Create schema
await _connection.ExecuteAsync(@"
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id VARCHAR(50) NOT NULL,
total NUMERIC(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)");
}
public async Task DisposeAsync()
{
await _connection.DisposeAsync();
await _dbContainer.DisposeAsync();
}
[Fact]
public async Task PostgreSql_ShouldHandleTransactions()
{
using var transaction = await _connection.BeginTransactionAsync();
await _connection.ExecuteAsync(
"INSERT INTO orders (customer_id, total) VALUES (@CustomerId, @Total)",
new { CustomerId = "CUST1", Total = 100.00m },
transaction);
await transaction.RollbackAsync();
var count = await _connection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM orders");
Assert.Equal(0, count); // Rollback should prevent insert
}
}Testing Migrations with Real Databases
public class MigrationTests : IAsyncLifetime
{
private readonly TestcontainersContainer _container;
private string _connectionString;
public async Task InitializeAsync()
{
_container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SA_PASSWORD", "Your_password123")
.WithPortBinding(1433, true)
.Build();
await _container.StartAsync();
var port = _container.GetMappedPublicPort(1433);
_connectionString = $"Server=localhost,{port};Database=TestDb;User Id=sa;Password=Your_password123;TrustServerCertificate=true";
}
[Fact]
public async Task Migrations_ShouldRunSuccessfully()
{
// Run Entity Framework migrations
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlServer(_connectionString);
using var context = new AppDbContext(optionsBuilder.Options);
// Apply migrations
await context.Database.MigrateAsync();
// Verify schema
var canConnect = await context.Database.CanConnectAsync();
Assert.True(canConnect);
// Verify tables exist
var tables = await context.Database.SqlQueryRaw<string>(
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").ToListAsync();
Assert.Contains("Orders", tables);
Assert.Contains("Customers", tables);
}
public async Task DisposeAsync()
{
await _container.DisposeAsync();
}
}Infrastructure Testing Patterns
Patterns for testing Redis, RabbitMQ, multi-container networks, container reuse, and database reset with Respawn.
Contents
- Redis Integration Tests
- RabbitMQ Integration Tests
- Multi-Container Networks
- Reusing Containers Across Tests
- Database Reset with Respawn
Redis Integration Tests
public class RedisTests : IAsyncLifetime
{
private readonly TestcontainersContainer _redisContainer;
private IConnectionMultiplexer _redis;
public RedisTests()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithPortBinding(6379, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
var port = _redisContainer.GetMappedPublicPort(6379);
_redis = await ConnectionMultiplexer.ConnectAsync($"localhost:{port}");
}
public async Task DisposeAsync()
{
await _redis.DisposeAsync();
await _redisContainer.DisposeAsync();
}
[Fact]
public async Task Redis_ShouldCacheValues()
{
var db = _redis.GetDatabase();
await db.StringSetAsync("key1", "value1");
var value = await db.StringGetAsync("key1");
Assert.Equal("value1", value.ToString());
}
[Fact]
public async Task Redis_ShouldExpireKeys()
{
var db = _redis.GetDatabase();
await db.StringSetAsync("temp-key", "temp-value",
expiry: TimeSpan.FromSeconds(1));
Assert.True(await db.KeyExistsAsync("temp-key"));
await Task.Delay(1100);
Assert.False(await db.KeyExistsAsync("temp-key"));
}
}RabbitMQ Integration Tests
public class RabbitMqTests : IAsyncLifetime
{
private readonly TestcontainersContainer _rabbitContainer;
private IConnection _connection;
public RabbitMqTests()
{
_rabbitContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("rabbitmq:management-alpine")
.WithPortBinding(5672, true)
.WithPortBinding(15672, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5672))
.Build();
}
public async Task InitializeAsync()
{
await _rabbitContainer.StartAsync();
var port = _rabbitContainer.GetMappedPublicPort(5672);
var factory = new ConnectionFactory
{
HostName = "localhost",
Port = port,
UserName = "guest",
Password = "guest"
};
_connection = await factory.CreateConnectionAsync();
}
public async Task DisposeAsync()
{
await _connection.CloseAsync();
await _rabbitContainer.DisposeAsync();
}
[Fact]
public async Task RabbitMq_ShouldPublishAndConsumeMessage()
{
using var channel = await _connection.CreateChannelAsync();
var queueName = "test-queue";
await channel.QueueDeclareAsync(queueName, durable: false,
exclusive: false, autoDelete: true);
var message = "Hello, RabbitMQ!";
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(exchange: "",
routingKey: queueName,
body: body);
var consumer = new EventingBasicConsumer(channel);
var tcs = new TaskCompletionSource<string>();
consumer.Received += (model, ea) =>
{
var receivedMessage = Encoding.UTF8.GetString(ea.Body.ToArray());
tcs.SetResult(receivedMessage);
};
await channel.BasicConsumeAsync(queueName, autoAck: true,
consumer: consumer);
var received = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(message, received);
}
}Multi-Container Networks
When you need multiple containers to communicate:
public class MultiContainerTests : IAsyncLifetime
{
private readonly INetwork _network;
private readonly TestcontainersContainer _dbContainer;
private readonly TestcontainersContainer _redisContainer;
public MultiContainerTests()
{
_network = new TestcontainersNetworkBuilder()
.Build();
_dbContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:latest")
.WithNetwork(_network)
.WithNetworkAliases("db")
.WithEnvironment("POSTGRES_PASSWORD", "postgres")
.Build();
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithNetwork(_network)
.WithNetworkAliases("redis")
.Build();
}
public async Task InitializeAsync()
{
await _network.CreateAsync();
await Task.WhenAll(
_dbContainer.StartAsync(),
_redisContainer.StartAsync());
}
public async Task DisposeAsync()
{
await Task.WhenAll(
_dbContainer.DisposeAsync().AsTask(),
_redisContainer.DisposeAsync().AsTask());
await _network.DisposeAsync();
}
[Fact]
public async Task Containers_CanCommunicate()
{
// Both containers can reach each other via network aliases
// db -> redis://redis:6379
// redis -> postgres://db:5432
}
}Reusing Containers Across Tests
For faster test execution, reuse containers across tests in a class:
[Collection("Database collection")]
public class FastDatabaseTests
{
private readonly DatabaseFixture _fixture;
public FastDatabaseTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Test1()
{
// Use _fixture.Connection
}
[Fact]
public async Task Test2()
{
// Reuses the same container
}
}
// Shared fixture
public class DatabaseFixture : IAsyncLifetime
{
private readonly TestcontainersContainer _container;
public IDbConnection Connection { get; private set; }
public DatabaseFixture()
{
_container = 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 _container.StartAsync();
// Setup connection
}
public async Task DisposeAsync()
{
await Connection.DisposeAsync();
await _container.DisposeAsync();
}
}
[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }Database Reset with Respawn
When reusing containers, use Respawn to reset database state between tests:
<PackageReference Include="Respawn" Version="*" />Basic Respawn Setup
using Respawn;
public class DatabaseFixture : IAsyncLifetime
{
private readonly TestcontainersContainer _container;
private Respawner _respawner = null!;
public NpgsqlConnection Connection { get; private set; } = null!;
public string ConnectionString { get; private set; } = null!;
public async Task InitializeAsync()
{
await _container.StartAsync();
var port = _container.GetMappedPublicPort(5432);
ConnectionString = $"Host=localhost;Port={port};Database=testdb;Username=postgres;Password=postgres";
Connection = new NpgsqlConnection(ConnectionString);
await Connection.OpenAsync();
await RunMigrationsAsync();
_respawner = await Respawner.CreateAsync(ConnectionString, new RespawnerOptions
{
TablesToIgnore = new Table[]
{
"__EFMigrationsHistory",
"AspNetRoles",
"schema_version"
},
DbAdapter = DbAdapter.Postgres
});
}
public async Task ResetDatabaseAsync()
{
await _respawner.ResetAsync(ConnectionString);
}
public async Task DisposeAsync()
{
await Connection.DisposeAsync();
await _container.DisposeAsync();
}
}Using Respawn in Tests
[Collection("Database collection")]
public class OrderTests : IAsyncLifetime
{
private readonly DatabaseFixture _fixture;
public OrderTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
public async Task InitializeAsync()
{
await _fixture.ResetDatabaseAsync();
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateOrder_ShouldPersist()
{
await _fixture.Connection.ExecuteAsync(
"INSERT INTO orders (customer_id, total) VALUES (@CustomerId, @Total)",
new { CustomerId = "CUST1", Total = 100.00m });
var count = await _fixture.Connection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM orders");
Assert.Equal(1, count);
}
[Fact]
public async Task AnotherTest_StartsWithCleanDatabase()
{
var count = await _fixture.Connection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM orders");
Assert.Equal(0, count); // Clean slate!
}
}Respawn Options
var respawner = await Respawner.CreateAsync(connectionString, new RespawnerOptions
{
TablesToIgnore = new Table[]
{
"__EFMigrationsHistory",
new Table("public", "lookup_data"),
},
SchemasToInclude = new[] { "public", "app" },
SchemasToExclude = new[] { "audit", "logging" },
DbAdapter = DbAdapter.Postgres,
WithReseed = true
});Why Respawn Over Container Recreation
| Approach | Pros | Cons |
|---|---|---|
| New container per test | Complete isolation | Slow (10-30s per container) |
| Respawn | Fast (~50ms), preserves schema/migrations | Requires careful table exclusion |
| Transaction rollback | Fastest | Can't test commit behavior |
Related skills
FAQ
What does testcontainers-integration-tests spin up?
testcontainers-integration-tests spins up real databases, message queues, and dependent services in disposable Docker containers for .NET integration tests using Testcontainers.
Which .NET test frameworks does testcontainers-integration-tests target?
testcontainers-integration-tests targets .NET integration test workflows such as xUnit and NUnit with Testcontainers, not unit-only mocks or non-.NET Testcontainers projects.