Testes de integração em .NET: porque mockar o mundo inteiro não prova muita coisa
Uma suíte de testes toda verde é uma sensação boa. O problema é quando essa sensação vem de testar não o sistema, mas uma versão dele onde cada peça que poderia realmente falhar foi substituída por um dublê que sempre faz exatamente o que o teste espera.
O que mock excessivo realmente esconde
Mockar um repositório, um banco de dados ou uma API externa é uma técnica legítima para isolar a unidade de código sob teste, evitando que o teste dependa de infraestrutura externa para rodar. O problema aparece quando esse isolamento vira regra geral aplicada a tudo, inclusive à integração entre as próprias peças da aplicação: se o repositório está mockado, o banco também, e o cliente HTTP também, o que sobra sendo testado é, na prática, só a lógica de orquestração entre chamadas simuladas, não o comportamento real do sistema quando essas peças interagem de verdade.
Isso produz testes frágeis (brittle tests), que quebram toda vez que um detalhe de implementação muda, mesmo sem o comportamento externo do sistema ter mudado nada, porque o teste está amarrado à forma como o mock foi configurado, não ao resultado real que importa. E produz falso positivo: o teste passa porque o mock foi configurado para devolver exatamente o que o teste esperava, não porque o sistema real, com um banco real por trás, se comportaria daquele jeito.
A diferença entre unitário e integração não é sobre quantidade de mock
Um teste unitário isola uma unidade pequena de lógica, tipicamente uma classe ou método, de suas dependências externas, verificando um comportamento específico e determinístico dessa unidade isoladamente. Um teste de integração verifica como partes do sistema realmente se comportam quando combinadas, incluindo, na medida do possível, infraestrutura real ou próxima da real, banco de dados, cliente HTTP, sistema de arquivos.
Nenhuma das duas abordagens é superior de forma absoluta. O problema é depender só da primeira e tratar isso como cobertura suficiente para todo o sistema, quando boa parte dos bugs reais em produção nasce exatamente na fronteira entre componentes, que é o espaço que teste unitário com tudo mockado, por definição, nunca alcança.
WebApplicationFactory: testar a aplicação inteira, sem hospedar nada externo
No ASP.NET Core, WebApplicationFactory<TEntryPoint> permite subir a aplicação inteira em memória, incluindo pipeline de middleware, injeção de dependência configurada como em produção, e roteamento real, para então enviar requisições HTTP de verdade contra ela dentro do teste:
public class PedidosApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task CriarPedido_DeveRetornarCreated()
{
var client = factory.CreateClient();
var response = await client.PostAsJsonAsync("/pedidos", new { ClienteId = 1, Itens = new[] { "produto-a" } });
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
Esse teste passa pelo pipeline real de request, incluindo validação, roteamento e serialização, coisas que um teste com controller instanciado manualmente e dependências mockadas simplesmente não exercita, mesmo que a lógica interna do controller esteja coberta de outra forma.
Substituir só o que realmente precisa ser substituído
WebApplicationFactory também permite trocar serviços específicos via WithWebHostBuilder, substituindo, por exemplo, apenas a configuração de um serviço externo de pagamento por um dublê controlado, mantendo banco de dados e o restante do pipeline reais. Essa é a diferença prática entre um teste de integração bem desenhado e um teste unitário disfarçado de integração: mockar apenas a fronteira que realmente não faz sentido acionar de verdade num teste automatizado (um serviço de terceiro pago, por exemplo), sem estender esse mesmo tratamento para peças internas do próprio sistema que deveriam estar sendo verificadas de verdade.
O verde da suíte não é o objetivo, é um efeito colateral
Uma suíte cheia de mock aumenta a velocidade de execução e reduz a superfície de falha por dependência externa instável, o que parece atraente. O custo é que ela também reduz a capacidade de detectar problema real, exatamente o motivo pelo qual a suíte existe. Testes de integração, cobrindo pelo menos os caminhos críticos com infraestrutura real ou próxima da real, são o complemento que evita que “toda a suíte passou” vire sinônimo de “não sei se isso funciona de verdade”.