Implementando Testes de Aceitação para Validar Requisitos em Aplicações .NET com Clean Architecture
A validação rigorosa de que uma aplicação atende corretamente aos seus requisitos de negócio é um pilar fundamental do desenvolvimento de software de qualidade. Em projetos baseados na Clean Architecture, a estrutura em camadas bem definidas oferece oportunidades únicas para implementar uma estratégia de testes de aceitação eficaz e organizada.
Princípios dos Testes de Aceitação em Arquiteturas em Camadas
A abordagem da pirâmide de testes adapta-se perfeitamente à Clean Architecture, permitindo uma verificação granular e em múltiplos níveis de abstração. Testes unitários validam a lógica de domínio isolada, testes de integração verificam a correta comunicação entre camadas e componentes, enquanto os testes de aceitação (ou funcionais) garantem que os fluxos completos de usuário e as regras de negócio de alto nível estão implementadas conforme o especificado.
Configuração de um Ambiente de Teste Isolado
Para executar testes de integração e aceitação de forma confiável, é crucial configurar um ambiente de hospedagem web dedicado. A classe a seguir demonstra como substituir serviços de produção, como o banco de dados, por implementações de teste (como um banco em memória), garantindo o isolamento entre testes e a reprodutibilidade.
public class TestWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder configuracao)
{
configuracao.ConfigureServices(servicos =>
{
// Remove a configuração de DbContext existente
var descriptor = servicos.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
{
servicos.Remove(descriptor);
}
// Adiciona o banco de dados em memória para os testes
servicos.AddDbContext<AppDbContext>(opcoes =>
{
opcoes.UseInMemoryDatabase("BancoTeste");
});
});
}
}
Escrevendo Testes de Aceitação para Funcionaldiades de API
Utilizando um framework como o xUnit, podemos criar testes que simulam requisições HTTP reais e validam as respostas. O exemplo abaixo verifica o endpoint para listar todos os contribuidores, assegurando que a resposta contém os dados esperados.
public class TestesEndpointContribuidores : IClassFixture<TestWebApplicationFactory<Program>>
{
private readonly HttpClient _clienteHttp;
private readonly TestWebApplicationFactory<Program> _fabrica;
public TestesEndpointContribuidores(TestWebApplicationFactory<Program> fabrica)
{
_fabrica = fabrica;
_clienteHttp = fabrica.CreateClient();
}
[Fact]
public async Task DeveRetornarListaDeContribuidores_QuandoRequisicaoForValida()
{
// Ação
var resposta = await _clienteHttp.GetAsync("/api/contribuidores");
// Verificação
resposta.EnsureSuccessStatusCode();
var conteudo = await resposta.Content.ReadAsStringAsync();
var lista = JsonSerializer.Deserialize<List<ContribuidorDto>>(conteudo);
Assert.NotNull(lista);
Assert.True(lista.Count > 0, "A lista de contribuidores não deve estar vazia.");
Assert.Contains(lista, c => c.Nome == "Contribuidor de Teste 1");
}
}
Gerenciamento de Dados de Teste
Uma estratégia sólida para gerenciar o estado dos dados é essencial. Em vez de depender de dados compartilhados entre testes, é recomendável criar e popular um banco de dados limpo para cada teste ou conjunto de testes relacionados. A classe abaixo encapsula a lógica para inicializar o banco de dados de teste.
public static class DadosDeTesteIniciais
{
public static readonly Contribuidor ContribuidorAlpha = new() { Nome = "Alpha", Telefone = "111-111" };
public static readonly Contribuidor ContribuidorBeta = new() { Nome = "Beta", Telefone = "222-222" };
public static void PopularBanco(AppDbContext contexto)
{
if (contexto.Contribuidores.Any())
{
return; // Dados já existem, não fazer nada
}
contexto.Contribuidores.AddRange(ContribuidorAlpha, ContribuidorBeta);
contexto.SaveChanges();
}
}
Organização e Boas Práticas
Organizar os testes de forma clara melhora a manutenibilidade. Utilizar a nomenclatura Dado_Quando_Então nos métodos de teste torna a intenção do teste imediatamente compreensível. Por exemplo: DeveRetornarStatus404_QuandoContribuidorNaoExistir.
Para testes que dependem de um estado compartilhado ou que não podem ser executados em paralelo (como testes que usam o mesmo banco de dados em memória), a utilização de coleções de teste é indicada:
[Collection("Testes Sequenciais")]
public class TestesCRUDContribuidor : IClassFixture<TestWebApplicationFactory<Program>>
{
// ... corpo dos testes
}
Integração com Pipeline de CI/CD
Os testes de aceitação devem ser um componente automático e obrigatório no pipeline de integração contínua. Um passo típico em um workflow do GitHub Actions seria:
- name: Executar Testes
run: dotnet test --filter "Category=Acceptance" --logger "trx;LogFileName=resultado-testes.trx"
Este comando executa apenas os testes categorizados como de aceitação e gera um arquivo de log estruturado. Estabelecer métricas de cobertura de código e de cenários de negócio validados ajuda a medir a saúde do projeto e a confiança nas entregas.
Considerações sobre Desempenho e Dados
Testes de aceitação, por sua natureza, são mais lentos que testes unitários. É importante monitorar seu tempo de execução e considerar estratégias como a execução paralela controlada ou a segmentação de testes por módulo funcional. O uso de banco de dados em memória para testes de integração acelera significativamente a execução, eliminando a latência de I/O de disco, mas requer atenção para garantir que o comportamento reflita fielmente o provedor de banco de dados de produção em aspectos críticos.