Fundamentos da Automação de Testes
Por que automatizar
Seção intitulada “Por que automatizar”Testes manuais não escalam: são lentos, não repetíveis com precisão e dependem de atenção humana justamente onde ela falha (repetição). A automação transforma verificação em um ativo executável que roda em segundos, em cada commit, sem custo marginal.
Os ganhos concretos:
- Feedback rápido: descobrir a quebra segundos após introduzi-la, com o contexto ainda fresco, é ordens de grandeza mais barato que descobrir em produção.
- Rede de segurança para refatorar: sem testes, toda mudança estrutural é uma aposta. Com eles, refatorar vira rotina.
- Documentação executável: um teste bem nomeado descreve o que o sistema faz e não deixa a documentação envelhecer, porque a build quebra se ela mentir.
- Pressão de design: código difícil de testar é código mal projetado. A dor do teste é um sinal precoce, tema de Testabilidade e Arquitetura.
O que é um bom teste
Seção intitulada “O que é um bom teste”Um bom teste equilibra quatro pilares (Vladimir Khorikov os sistematiza bem):
- Proteção contra regressões — o quanto o teste detecta bugs reais.
- Resistência a refactoring — o quanto ele evita alarmes falsos quando você muda a implementação sem mudar o comportamento.
- Feedback rápido — o quanto ele roda rápido.
- Manutenibilidade — o quanto é fácil de ler e de manter.
Há tensão inerente: testes acoplados a detalhes internos (excesso de mocks) ganham feedback rápido, mas perdem resistência a refactoring. O alvo é testar comportamento observável pela interface pública, não a mecânica interna. As propriedades operacionais desse ideal estão no Princípio FIRST.
Estrutura: Arrange-Act-Assert / Given-When-Then
Seção intitulada “Estrutura: Arrange-Act-Assert / Given-When-Then”Todo teste tem três fases. Deixá-las visualmente separadas é o hábito de maior retorno na legibilidade.
- Arrange (Given): prepara o estado e as dependências.
- Act (When): executa a única ação sob teste.
- Assert (Then): verifica o resultado.
Regras de ouro: um Act por teste e um conceito verificado por teste (não necessariamente um único assert, mas uma única razão para falhar).
import org.junit.jupiter.api.Test;import static org.assertj.core.api.Assertions.assertThat;import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CarrinhoTest {
@Test void deveSomarPrecoDosItensAdicionados() { var carrinho = new Carrinho(); carrinho.adicionar(new Item("Café", new BigDecimal("12.50"))); carrinho.adicionar(new Item("Filtro", new BigDecimal("7.50")));
BigDecimal total = carrinho.total();
assertThat(total).isEqualByComparingTo("20.00"); }
@Test void deveRejeitarItemComPrecoNegativo() { var carrinho = new Carrinho();
assertThatThrownBy(() -> carrinho.adicionar(new Item("Bug", new BigDecimal("-1")))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("preço"); }}O nome do método é parte do teste: deveSomarPrecoDosItensAdicionados comunica a regra sem que se leia o corpo. Prefira nomes que descrevem o comportamento esperado, não a mecânica (testTotal diz nada).
Determinismo
Seção intitulada “Determinismo”Um teste que às vezes passa e às vezes falha sem mudança no código é um teste flaky, e é pior que teste nenhum: erode a confiança na suíte inteira. Um único flaky treina o time a ignorar vermelho.
Fontes comuns de não-determinismo e como neutralizá-las:
- Tempo real — nunca use
LocalDateTime.now()direto. Injete umClock:
class AssinaturaTest {
@Test void deveExpirarTrintaDiasAposCriacao() { Clock relogio = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC); var assinatura = new Assinatura(relogio);
LocalDate expiracao = assinatura.dataDeExpiracao();
assertThat(expiracao).isEqualTo(LocalDate.of(2026, 1, 31)); }}- Ordem de iteração — não dependa da ordem de
HashMap/HashSet. UseassertThat(colecao).containsExactlyInAnyOrder(...). - Concorrência e sleeps —
Thread.sleepem teste é um cheiro; prefiraAwaitilitycom polling e timeout. - Aleatoriedade — injete o
Random/seed ou a fonte de UUID. - Estado compartilhado entre testes — cada teste deve montar seu próprio mundo (ver Independent no Princípio FIRST).
Custo x confiança
Seção intitulada “Custo x confiança”Testes não são grátis: custam para escrever, para rodar e para manter quando o código muda. A pergunta certa nunca é “temos testes?”, e sim “cada teste paga seu custo em confiança?”.
graph LR
A[Sem testes] -->|barato agora, caro depois| B[Medo de mudar]
C[Testes certos] -->|custo moderado| D[Confiança para mudar]
E[Testes demais / frágeis] -->|caro sempre| F[Suíte que atrapalha]
Testes acoplados a detalhes de implementação têm confiança baixa e custo alto — quebram a cada refatoração legítima. É o pior quadrante. O objetivo é maximizar confiança por unidade de custo, o que empurra para testar comportamento e para a base da pirâmide (Tipos de Teste - Unit, Integration e E2E).
Cobertura: útil como bússola, péssima como meta
Seção intitulada “Cobertura: útil como bússola, péssima como meta”Cobertura de código mede quais linhas foram executadas durante os testes. É um indicador de ausência, não de presença de qualidade: 100% de cobertura garante apenas que toda linha rodou, jamais que os asserts verificam algo relevante.
@Testvoid tautologicoComCoberturaFalsa() { var calc = new CalculadoraImposto(); BigDecimal r = calc.calcular(new BigDecimal("100")); assertThat(r).isNotNull();}O teste acima cobre 100% do método e não verifica nada — passaria mesmo que a alíquota estivesse errada. Cobertura sobe, confiança não.
Diretrizes pragmáticas:
- Use cobertura para achar buracos (código sem nenhum teste), não para bater metas de percentual.
- Metas rígidas de cobertura incentivam testes tautológicos — a métrica vira alvo e deixa de ser boa métrica (Lei de Goodhart).
- Para medir se os testes de fato pegam bugs, use mutation testing (PIT), que altera o código e verifica se algum teste falha. Isso está em Testes com IA.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Assert-roulette: muitos asserts sem mensagem, impossível saber qual falhou. Um conceito por teste.
- Teste tautológico:
assertThat(x).isNotNull()como único assert; cobre linha, não verifica comportamento. - Lógica no teste:
if,fore cálculos no teste replicam a implementação e escondem bugs; prefira valores literais esperados. - Testar o mock: quando o único assert é
verify(mock)...sobre uma chamada trivial, você testa a fiação, não a regra. - Nome genérico:
test1,testMetodo— o nome deveria ser a especificação. - Teste dependente de ordem/tempo: introduz flakiness e viola o determinismo.
- Perseguir 100% de cobertura: gera ruído e testes frágeis sem confiança real.
Relações
Seção intitulada “Relações”- Testes e TDD
- Princípio FIRST
- Tipos de Teste - Unit, Integration e E2E
- Test-Driven Development
- Testabilidade e Arquitetura
- Clean Code e Refactoring
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Por que 100% de cobertura não significa qualidade de testes?
Resposta
Cobertura mede apenas linhas executadas, não a força dos asserts. É possível ter 100% com testes tautológicos que nunca falhariam diante de um bug. Cobertura serve para localizar código sem teste algum; para avaliar poder de detecção, use mutation testing.
2. Qual o problema de usar LocalDateTime.now() dentro da lógica de produção?
Resposta
Torna o comportamento dependente do relógio real, quebrando o determinismo do teste. A correção é injetar um Clock (ex.: Clock.fixed), permitindo controlar o tempo no teste e tornando-o repetível.
3. O que significa “testar comportamento e não implementação” e por que importa?
Resposta
Significa verificar a saída observável pela interface pública, não os passos internos. Importa porque testes acoplados a detalhes internos quebram em toda refatoração legítima (baixa resistência a refactoring), gerando alarmes falsos e alto custo de manutenção.
4. Por que um teste flaky é pior que nenhum teste?
Resposta
Ele falha de forma intermitente sem quebra real, treinando o time a ignorar o vermelho. Isso corrói a confiança em toda a suíte: quando as pessoas param de acreditar nos testes, a suíte deixa de proteger contra regressões.