Pular para o conteúdo

Fundamentos da Automação de Testes

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.

Um bom teste equilibra quatro pilares (Vladimir Khorikov os sistematiza bem):

  1. Proteção contra regressões — o quanto o teste detecta bugs reais.
  2. Resistência a refactoring — o quanto ele evita alarmes falsos quando você muda a implementação sem mudar o comportamento.
  3. Feedback rápido — o quanto ele roda rápido.
  4. 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.

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).

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 um Clock:
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. Use assertThat(colecao).containsExactlyInAnyOrder(...).
  • Concorrência e sleepsThread.sleep em teste é um cheiro; prefira Awaitility com 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).

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.

@Test
void 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.
  • 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, for e 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.

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.