Unit vs Integration Tests - Best Practices¶
Visão Geral¶
Este documento define as melhores práticas para testes unitários e de integração no projeto MeAjudaAi.
Diferença entre Unit e Integration Tests¶
Unit Tests¶
- Objetivo: Testar uma única unidade de código isoladamente
- Infraestrutura: Sem dependências externas (banco de dados, APIs, filas)
- Velocidade: Muito rápidos (< 100ms por teste)
- Mocks: Usa mocks/stubs para todas as dependências
- Coverage: Deve cobrir toda a lógica de negócio e edge cases
Integration Tests¶
- Objetivo: Testar integração entre componentes e infraestrutura
- Infraestrutura: Usa banco de dados real, message brokers, etc.
- Velocidade: Mais lentos (100ms - 5s por teste)
- Mocks: Mínimo de mocks, apenas para serviços externos (APIs de terceiros)
- Coverage: Valida comportamento end-to-end com infraestrutura real
❌ Anti-Patterns Comuns¶
1. Usar InMemory Database para Testes Unitários de EF Core Configuration¶
Problema:
// ❌ ERRADO - InMemory não valida configurações reais
[Fact]
public void Configure_ShouldConvertEnumsToInt()
{
var options = new DbContextOptionsBuilder<MyContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
using var context = new MyContext(options);
var entityType = context.Model.FindEntityType(typeof(MyEntity));
// InMemory não popula GetValueConverter() mesmo com conversores configurados
entityType.FindProperty("Status").GetValueConverter().Should().NotBeNull(); // ❌ FALHA
}
Limitações do InMemory Provider: - ❌ Não valida constraints (FK, Unique, Check) - ❌ Não executa migrations - ❌ Não popula value converters - ❌ Não valida tipos de coluna do banco real (jsonb, geography, etc.) - ❌ Comportamento diferente do PostgreSQL (case sensitivity, null handling, etc.)
Solução:
// ✅ CORRETO - Teste de integração com banco real
[Fact]
public async Task Configure_ShouldConvertEnumsToInt()
{
// Usa PostgreSQL real via Testcontainers ou Docker
await using var factory = new CustomWebApplicationFactory();
await using var scope = factory.Services.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<MyContext>();
// Teste com banco real valida conversores, constraints, etc.
var entity = new MyEntity { Status = MyStatus.Active };
context.Add(entity);
await context.SaveChangesAsync();
// Verifica no banco que o enum foi convertido para int
var rawSql = await context.Database.GetDbConnection()
.QueryAsync<int>("SELECT status FROM my_table WHERE id = @id", new { id = entity.Id });
rawSql.Should().Be((int)MyStatus.Active);
}
2. Testes E2E que apenas repetem testes de integração¶
Problema:
// ❌ REDUNDANTE - Este teste E2E apenas repete o teste de integração
[Fact]
public async Task CreateDocument_ShouldReturn201()
{
var client = _factory.CreateClient();
var response = await client.PostAsync("/api/documents", content);
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
Testes E2E devem validar fluxos completos (múltiplos endpoints, eventos, side effects), não apenas endpoints individuais já cobertos por testes de integração.
✅ Quando Usar Cada Tipo de Teste¶
Use Unit Tests para:¶
-
Lógica de Domínio (Entities, Value Objects, Domain Events)
[Fact] public void Document_Verify_ShouldSetVerifiedAt() { var document = Document.Create(providerId, documentType, fileUrl, fileName); var now = DateTime.UtcNow; document.Verify(); document.VerifiedAt.Should().BeCloseTo(now, TimeSpan.FromSeconds(1)); document.Status.Should().Be(EDocumentStatus.Verified); } -
Application Handlers (Commands, Queries)
[Fact] public async Task Handle_ValidCommand_ShouldCreateDocument() { // Arrange - Mock repositories var repositoryMock = new Mock<IDocumentRepository>(); var handler = new CreateDocumentCommandHandler(repositoryMock.Object); // Act var result = await handler.Handle(command); // Assert repositoryMock.Verify(x => x.AddAsync(It.IsAny<Document>()), Times.Once); } -
Domain Event Handlers
[Fact] public async Task HandleAsync_ShouldPublishIntegrationEvent() { // Arrange - Mock message bus var messageBusMock = new Mock<IMessageBus>(); var handler = new DocumentVerifiedDomainEventHandler(messageBusMock.Object); // Act await handler.HandleAsync(domainEvent); // Assert messageBusMock.Verify(x => x.PublishAsync( It.Is<DocumentVerifiedIntegrationEvent>(e => e.DocumentId == domainEvent.AggregateId)), Times.Once); } -
Validators, Extensions, Helpers
Use Integration Tests para:¶
-
EF Core Configuration (Entities, Mappings, Migrations)
[Fact] public async Task DocumentConfiguration_ShouldStoreEnumsAsIntegers() { // Usa banco real await using var factory = new CustomWebApplicationFactory(); var context = factory.Services.GetRequiredService<DocumentsDbContext>(); var document = Document.Create(providerId, EDocumentType.RG, url, name); context.Add(document); await context.SaveChangesAsync(); // Verifica no banco var connection = context.Database.GetDbConnection(); var type = await connection.ExecuteScalarAsync<int>( "SELECT document_type FROM meajudaai_documents.documents WHERE id = @id", new { id = document.Id }); type.Should().Be(1); // RG = 1 } -
Repository Implementations
[Fact] public async Task GetByIdAsync_ExistingDocument_ShouldReturn() { await using var factory = new CustomWebApplicationFactory(); var repository = factory.Services.GetRequiredService<IDocumentRepository>(); // Seed data var document = await SeedDocument(); // Act var result = await repository.GetByIdAsync(document.Id); // Assert result.Should().NotBeNull(); result.FileUrl.Should().Be(document.FileUrl); } -
API Endpoints (Controllers)
[Fact] public async Task CreateDocument_ValidRequest_ShouldReturn201() { await using var factory = new CustomWebApplicationFactory(); var client = factory.CreateClient(); var request = new CreateDocumentRequest { ... }; var response = await client.PostAsJsonAsync("/api/documents", request); response.StatusCode.Should().Be(HttpStatusCode.Created); var document = await response.Content.ReadFromJsonAsync<DocumentResponse>(); document.Id.Should().NotBeEmpty(); } -
Message Bus Integration (Events, Queues)
[Fact] public async Task PublishAsync_ShouldSendMessageToQueue() { await using var factory = new CustomWebApplicationFactory(); var messageBus = factory.Services.GetRequiredService<IMessageBus>(); await messageBus.PublishAsync(new DocumentVerifiedIntegrationEvent(...)); // Verifica que a mensagem foi publicada no RabbitMQ/ServiceBus var consumer = factory.Services.GetRequiredService<TestMessageConsumer>(); var message = await consumer.WaitForMessageAsync<DocumentVerifiedIntegrationEvent>(); message.Should().NotBeNull(); }
Use E2E Tests para:¶
- Fluxos Completos de Negócio
[Fact] public async Task ProviderRegistration_CompleteFlow_ShouldSucceed() { // 1. Criar provider var createResponse = await client.PostAsync("/api/providers", providerData); var provider = await createResponse.Content.ReadFromJsonAsync<ProviderResponse>(); // 2. Upload de documento var uploadResponse = await client.PostAsync($"/api/documents", documentData); var document = await uploadResponse.Content.ReadFromJsonAsync<DocumentResponse>(); // 3. Verificar documento var verifyResponse = await client.PatchAsync($"/api/documents/{document.Id}/verify", null); // 4. Verificar que provider foi atualizado (via evento) await Task.Delay(2000); // Aguarda processamento assíncrono var providerResponse = await client.GetAsync($"/api/providers/{provider.Id}"); var updatedProvider = await providerResponse.Content.ReadFromJsonAsync<ProviderResponse>(); updatedProvider.IsVerified.Should().BeTrue(); }
Checklist de Decisão¶
Ao criar um novo teste, pergunte:
| Pergunta | Unit | Integration | E2E |
|---|---|---|---|
| Testa lógica isolada sem dependências? | ✅ | ❌ | ❌ |
| Precisa de banco de dados? | ❌ | ✅ | ✅ |
| Precisa de message broker? | ❌ | ✅ | ✅ |
| Testa múltiplos endpoints? | ❌ | ❌ | ✅ |
| Testa eventos assíncronos? | ❌ | ✅ | ✅ |
| Valida EF Core configuration? | ❌ | ✅ | ❌ |
| Executa em < 100ms? | ✅ | ❌ | ❌ |
| Usa mocks? | ✅ | Poucos | Mínimo |
Resumo¶
- ✅ Unit Tests: Lógica pura, mocks para tudo, rápidos
- ✅ Integration Tests: Banco real, repositórios, APIs, eventos
- ✅ E2E Tests: Fluxos completos multi-endpoint
- ❌ InMemory Database: Nunca para testes de configuração EF Core
- ❌ E2E Redundantes: Não duplicar testes de integração