O que Testes de Integração com TestContainers faz?
Quando Usar Este Skill
Use este skill quando:
- Escrever testes de integração que precisam de infraestrutura real (bancos de dados, caches, filas de mensagens)
- Testar camadas de acesso a dados em bancos de dados reais
- Verificar integrações com filas de mensagens
- Testar o comportamento de cache com Redis
- Evitar mocks para componentes de infraestrutura
- Garantir que os testes funcionem em ambientes semelhantes à produção
- Testar migrações e alterações de esquema de banco de dados
Arquivos de Referência
- database-patterns.md: Exemplos de testes com SQL Server, PostgreSQL e migrações
- infrastructure-patterns.md: Exemplos de Redis, RabbitMQ, redes multi-contêiner, reutilização de contêineres e Respawn
Princípios Fundamentais
- Infraestrutura Real em vez de Mocks - Use bancos de dados/serviços reais em contêineres, não mocks
- Isolamento de Testes - Cada teste recebe contêineres ou dados novos
- Limpeza Automática - TestContainers gerencia o ciclo de vida e a limpeza dos contêineres
- Inicialização Rápida - Reutilize contêineres entre testes na mesma classe quando apropriado
- Compatível com CI/CD - Funciona perfeitamente em ambientes CI com Docker habilitado
- Randomização de Portas - Contêineres usam portas aleatórias para evitar conflitos
Por Que TestContainers em vez de Mocks?
O Problema de Mockar Infraestrutura
// RUIM: Mockando um banco de dados
public class OrderRepositoryTests
{
private readonly Mock<IDbConnection> _mockDb = new();
[Fact]
public async Task GetOrder_ReturnsOrder()
{
// Isso não testa o comportamento real do SQL, restrições ou desempenho
_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);
}
}
Problemas: não testa consultas SQL reais, ignora restrições/índices, dá falsa confiança, não captura erros de sintaxe SQL.
Melhor: TestContainers com Banco de Dados Real
// BOM: Testando contra um banco de dados real
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);
}
}
Consulte database-patterns.md para exemplos completos de SQL Server, PostgreSQL e testes de migração.
Consulte infrastructure-patterns.md para exemplos de Redis, RabbitMQ, redes multi-contêiner, reutilização de contêineres e padrões de reset de banco de dados com Respawn.
Pacotes NuGet Necessários
<ItemGroup>
<PackageReference Include="Testcontainers" Version="*" />
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
<!-- Pacotes específicos de banco de dados -->
<PackageReference Include="Microsoft.Data.SqlClient" Version="*" />
<PackageReference Include="Npgsql" Version="*" /> <!-- Para PostgreSQL -->
<PackageReference Include="MySqlConnector" Version="*" /> <!-- Para MySQL -->
<!-- Outras infraestruturas -->
<PackageReference Include="StackExchange.Redis" Version="*" /> <!-- Para Redis -->
<PackageReference Include="RabbitMQ.Client" Version="*" /> <!-- Para RabbitMQ -->
</ItemGroup>
Melhores Práticas
- Sempre Use IAsyncLifetime - Configuração e desmontagem assíncrona adequadas
- Aguarde a Disponibilidade da Porta - Use
WaitStrategypara garantir que os contêineres estejam prontos - Use Portas Aleatórias - Deixe o TestContainers atribuir portas automaticamente
- Limpe Dados entre Testes - Use contêineres novos ou trunque tabelas
- Reutilize Contêineres Quando Possível - Mais rápido do que criar novos para cada teste
- Teste Consultas Reais - Não teste apenas mocks; verifique o comportamento real do SQL
- Verifique Restrições - Teste chaves estrangeiras, restrições únicas, índices
- Teste Transações - Verifique o comportamento de rollback e commit
- Use Dados Realistas - Teste com volumes de dados semelhantes aos de produção
- Lide com a Limpeza - Sempre descarte os contêineres em
DisposeAsync
Problemas Comuns e Soluções
Tempo Limite de Inicialização do Contêiner
_container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("postgres:latest")
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(5432)
.WithTimeout(TimeSpan.FromMinutes(2)))
.Build();
Porta Já em Uso
Sempre use mapeamento de porta aleatório:
.WithPortBinding(5432, true) // true = atribuir porta pública aleatória
Contêineres Não Limpos
Garanta a disposição adequada:
public async Task DisposeAsync()
{
await _connection?.DisposeAsync();
await _container?.DisposeAsync();
}
Testes Falham no CI Mas Passam Localmente
Garanta que o CI tenha suporte Docker:
# GitHub Actions
runs-on: ubuntu-latest # Tem Docker pré-instalado
Integração com CI/CD
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 -f
Dicas de Desempenho
- Reutilize contêineres - Compartilhe fixtures entre testes de uma coleção
- Use Respawn - Reinicie os dados sem recriar contêineres
- Execução paralela - TestContainers lida com conflitos de porta automaticamente
- Use imagens leves - Versões Alpine são menores e mais rápidas
- Cache de imagens - O Docker armazenará as imagens baixadas localmente