Pergunte para dez engenheiros sêniores qual deveria ser a proporção entre testes unitários, integração e end-to-end e você terá pelo menos quatro respostas convictas e mutuamente incompatíveis. A "pirâmide de testes" de Mike Cohn virou doutrina nos anos 2000; recebeu rebatidas modernas como o "Trophy" de Kent C. Dodds e o "Honeycomb" da Spotify. Cada uma reflete um contexto diferente e prioriza trade-offs distintos. Saber quando aplicar qual é parte do que separa engenheiros experientes — eles não escolhem por lealdade ao guru, escolhem por análise do sistema diante deles.
Este conceito não vai resolver o debate por você — é genuinamente contextual. Vai dar três coisas: o vocabulário preciso de cada estratégia, os argumentos a favor e contra de cada uma, e um conjunto de heurísticas para decidir o mix do seu sistema específico. Com isso, quando alguém na sua equipe disser "deveríamos ter mais testes de integração", você consegue ter a conversa em vez de ficar preso em "mas a pirâmide diz...".
O que cada nível de teste é
Antes do debate de proporções, alinhar os termos. Em conversas técnicas, "teste de integração" significa coisas diferentes em times diferentes. As definições que vou usar:
- Teste unitário: testa uma unidade isolada (uma função, uma classe). Sem dependências externas reais (banco, rede, filesystem real). Dependências internas podem ser fakes ou reais. Roda em milissegundos. Determinístico.
- Teste de integração: testa um conjunto de componentes cooperando, com pelo menos uma dependência externa real (geralmente banco em container, ou serviço HTTP). Roda em segundos a poucos minutos. Pode ter alguma flakiness se mal-feito.
- Teste end-to-end (E2E): testa o sistema inteiro funcionando como um usuário real veria — UI, API, banco, integrações. Roda em minutos. Frágil. Caro de manter.
- Teste de contrato (vamos tratar no Conceito 07): verifica que dois serviços concordam sobre o formato da comunicação, sem precisar subir os dois juntos.
Há nomes alternativos circulando: "teste de componente", "teste de sistema", "teste de aceitação". A taxonomia de Toby Clemson em Testing Strategies in a Microservice Architecture tem mais níveis. Para fins desta discussão, os quatro acima cobrem o essencial.
A Pirâmide — Mike Cohn, 2009
Mike Cohn publicou em Succeeding with Agile a imagem icônica: uma pirâmide com três camadas. Base larga: testes unitários, muitos. Meio: testes de serviço/integração, médio. Topo estreito: UI/E2E, poucos. A intuição é que testes mais baixos são rápidos, baratos, determinísticos; mais altos são lentos, caros, frágeis.
Os argumentos a favor são bons:
- Velocidade do feedback: suíte de unidade roda em segundos. Você descobre que quebrou algo enquanto ainda está no contexto de mudança.
- Especificidade do diagnóstico: teste unitário falha, você sabe exatamente qual unidade está errada. E2E falha, pode ser qualquer uma das vinte coisas envolvidas.
- Custo de manutenção: testes unitários mudam pouco quando código muda; E2E mudam muito mais quando UI ou fluxos mudam.
- Determinismo: testes unitários são puros e estáveis. E2E têm dependências (rede, timing, estado) que produzem flakiness.
A pirâmide envelheceu razoavelmente bem. Para sistemas onde a maior parte da complexidade está em domínio (cálculos, regras de negócio, transformações), ela cabe — porque essa complexidade pode ser testada isoladamente, e o restante do sistema é "casca fina" que poucos E2E cobrem bem.
O Troféu — Kent C. Dodds, 2018
Kent C. Dodds, no contexto de aplicações React/web, propôs uma forma diferente: um troféu. Camadas, em proporção: pequeno topo "estática" (linter, type checking), camada de unidade, maior camada de integração, e finalmente E2E reduzido. A inversão sutil: a maior fatia vai para integração, não unidade.
O argumento dele:
- Testes unitários têm um problema escondido: podem todos passar enquanto o sistema inteiro está quebrado, porque cada um isola demais.
- Testes de integração têm melhor relação custo/confiança em sistemas web modernos. Eles testam código real interagindo com colaboradores reais (ou fakes próximos de real), pegando classes de bug que nenhum unitário pegaria.
- Mocks excessivos em testes unitários produzem "verificação de implementação", o que é frágil e tem valor de regressão limitado.
O troféu nasce do contexto de aplicações de UI rica onde a integração entre componentes é onde a complexidade vive. Para esse contexto, faz muito sentido. A questão é se generaliza para outros — e a resposta depende.
O Honeycomb — Spotify (e outros)
A Spotify popularizou (em blog post de 2018) uma terceira forma para arquitetura de microsserviços: o honeycomb. A ideia central é que em microsserviços, a unidade natural de teste é o serviço, não a classe interna. Você testa cada serviço como uma "caixa preta" via sua API, com fakes para serviços vizinhos.
Proporção do honeycomb: poucos testes unitários focados (apenas onde lógica é genuinamente complexa), grande quantidade de testes de integração no nível do serviço, e E2E mínimos cobrindo fluxos críticos. O argumento é parecido com o do Troféu: minimizar mocks, testar com colaboração real onde possível.
Onde Honeycomb brilha:
- Microsserviços onde cada serviço é "casca fina" sobre estado persistente — pouco domínio interno, muito CRUD com validação.
- Sistemas onde o valor está na composição: o serviço sozinho não faz muito, é a colaboração que importa.
- Codebases onde a aderência a princípios SOLID já criou pequenas peças bem-isoladas, e testes unitários adicionais virariam ruído.
Os argumentos contrários
Cada estratégia tem críticos articulados. Vale ouvi-los.
Contra a Pirâmide pura
Críticas válidas:
- Pode produzir "100% cobertura, 0% confiança": testes unitários com mocks demais verificam que o código foi escrito como foi escrito, não que ele resolve o problema. Sistema integrado pode estar quebrado.
- Encoraja sobre-isolamento: para satisfazer "tudo testável em unidade", você quebra o sistema em pedaços muitas vezes artificiais, criando indireção e mocks.
- Mocks que envelhecem: mock criado quando colaborador funcionava de jeito X. Colaborador mudou para Y. Teste continua passando com mock antigo, mas integração quebrou.
Contra o Troféu/Honeycomb (a inversão)
Críticas válidas:
- Testes de integração lentos esfomeiam o ciclo: se a maior parte da suíte é integração, rodar tudo demora. Feedback lento destrói o ciclo TDD.
- Determinismo cai: integração com banco real, container, rede — cada uma uma fonte potencial de flakiness. Em escala, isso fica sério.
- Diagnóstico fica difícil: integração falha, você precisa investigar quais das peças quebrou. Em unidade, está dado.
- Para domínio rico, é desperdício: lógica de negócio complexa é exatamente o que se testa bem em unidade. Forçar tudo via integração é overhead.
O critério unificador — qual o objetivo?
Dave Farley, em Modern Software Engineering, formula o problema de forma esclarecedora: testes existem para responder duas perguntas diferentes, e estratégias diferentes respondem cada uma melhor.
- "O design está certo?": TDD com testes unitários responde. O ciclo curto, o feedback imediato, a pressão sobre fronteiras — tudo isso é design, não verificação.
- "O sistema funciona?": testes de integração e E2E respondem. Eles verificam que o que foi construído faz o que deveria, ponta a ponta.
Ambos importam. Um sistema com bom design mas integrações quebradas é útil para quem? Um sistema que "funciona" mas é impossível de manter é sustentável por quanto tempo? A resposta saudável é: tenha as duas camadas, ajuste a proporção pelo seu sistema.
Heurísticas para decidir o mix
Em vez de aderir a uma estratégia específica, use as seguintes perguntas para calibrar a proporção do seu sistema:
Onde está a complexidade?
Se a complexidade do seu sistema está principalmente em:
- Lógica de domínio (cálculos, regras de negócio, transformações de dados, parsers): teste unitário é o ferramental adequado. Pirâmide cabe.
- Coreografia entre componentes (orquestração, workflows, sagas): teste de integração captura melhor.
- Camada de apresentação (UI, formulários, navegação): teste de integração de componentes (ex: React Testing Library) e E2E seletivo.
- Comunicação entre serviços: contract testing (próximo conceito) supera tanto unitário quanto integração full.
Quanto custa cada categoria?
Não custo de escrever — custo de manter e rodar. Times com CI lento descobrem rápido que mil testes de integração viram fila de horas. Times com infra automática para containers descobrem que integração é barata. Mensure no seu contexto.
Quão estável é a interface?
Componentes com interface estável (núcleo de domínio que muda pouco) sustentam testes unitários por muito tempo sem manutenção. Componentes com interface volátil (UI em produto novo) sofrem com testes acoplados a interface — tanto unitário quanto E2E. Aqui pode ser o caso de menos testes em geral até a interface estabilizar.
Quão crítico é o caminho?
Caminhos que se quebrarem custam reputação ou receita merecem proteção multi-camada. Login, checkout, pagamento — tenha unitário e integração e E2E. Caminhos secundários podem ter cobertura menor.
Em que ponto você está?
Sistema novo em exploração: pouco teste, muitos spikes. Sistema maduro em produção: cobertura sólida em todas as camadas. Migração: testes antes da migração que verificam comportamento atual (characterization tests), depois refactor. Cada fase pede mix diferente.
O modelo de Spotify — categorias por velocidade
Uma decomposição prática que muitos times usam (independente da forma geométrica preferida) é separar testes por velocidade de feedback:
- Tier 1 — Hot loop (segundos): roda no save/build local. Linter, type check, testes unitários. Falha aqui é caro? Sim, para o desenvolvedor — atrasa o ciclo.
- Tier 2 — PR check (minutos): roda no CI por PR. Adiciona testes de integração rápidos (banco em container), testes de contrato. Pode ter algumas dezenas a centenas, ainda manejável.
- Tier 3 — Pre-deploy (10s de minutos): roda antes do deploy ou em main após merge. Adiciona E2E selectivo, testes de smoke, testes de carga leve. Não bloqueia desenvolvimento; bloqueia produção.
- Tier 4 — Periódicos (horas): rodam noturnamente ou semanalmente. Mutation testing, testes de carga pesada, testes de compatibilidade extensa. Detectam degradação ao longo do tempo.
Essa classificação por velocidade frequentemente é mais útil do que classificação por escopo. Mesmo um teste "unitário" pode ser slow se tem setup elaborado; um "integração" bem-feito pode ser rápido. O que importa para o time é quanto tempo do feedback você gasta para que tipo de garantia.
Não obsessione sobre a forma geométrica. Concentre em três métricas operacionais: o ciclo TDD funciona (segundos)? O CI dá veredito antes do contexto se perder (10-15 min)? Bugs em produção são pegos por algum nível de teste antes de chegarem lá? Se sim aos três, sua estratégia está adequada — independente de chamarmos pirâmide, troféu ou outra coisa.
Os três níveis no projeto deste módulo
O sistema de pedidos do projeto vai ter as quatro categorias:
-
Unitários em
Orders.Domain: cálculo de total, validação de pedido, transições de estado. Centenas, todos sub-segundo. -
Integração em
Orders.Infra: repositório com Postgres real (Testcontainers). Verifica queries, transações, constraints. Algumas dezenas, em poucos minutos. - Contract entre Pedido e Pagamento: o serviço de Pagamento é mock no PR de Pedido, mas Pact garante que o contrato está sendo respeitado.
- E2E: 1-2 testes do "fluxo de criação de pedido feliz", usando WebApplicationFactory ou similar.
A proporção provavelmente vai parecer com troféu mais que pirâmide pura — porque o domínio é simples e as integrações são onde o valor está. Mas é decisão tomada com critério, não dogma.
Como praticar
- Auditoria do seu projeto atual. Categorize todos os testes por velocidade e escopo. Veja a forma. Ela faz sentido para o tipo de sistema?
- Mensure tempo de cada categoria. Quanto leva o ciclo TDD local? Quanto leva o CI por PR? Quanto leva o deploy? Identifique gargalos. Frequentemente um único teste lento estraga uma suíte inteira.
- Discuta forma com o time. Não há resposta correta absoluta. Mas a discussão revela suposições escondidas — alguns acham "deveria ter mais E2E", outros "menos". A conversa em si clarifica o que cada um valoriza.
Referências para aprofundar
- livro Succeeding with Agile — Mike Cohn (2009).
- livro Modern Software Engineering — Dave Farley (2021).
- livro Building Microservices (2nd ed.) — Sam Newman (2021).
- artigo Write Tests. Not Too Many. Mostly Integration. — Kent C. Dodds (2018).
- artigo Testing Strategies in a Microservice Architecture — Toby Clemson (Martin Fowler bliki).
- artigo Testing of Microservices — André Schaffer (Spotify, 2018).
- artigo The Practical Test Pyramid — Ham Vocke (Martin Fowler bliki).
- artigo Just Say No to More End-to-End Tests — Mike Wacker (Google Testing Blog, 2015).
- docs Cypress Testing Strategy.
- docs Testcontainers.
- vídeo Test Pyramid in Practice — Henry Coles (devoxx).
- vídeo Test Sizes — Google Testing Tech.