Anatomia de um bom teste

Resumo em uma linha

Um bom teste arruma o cenário, executa uma ação e verifica um comportamento (AAA), tem nome que conta o que e quando, falha por uma única razão e obedece ao F.I.R.S.T — Fast, Independent, Repeatable, Self-validating, Timely.

Escrever teste é fácil. Escrever um bom teste é outra coisa.

A diferença não está em testar mais. Está em testar de um jeito que, seis meses depois, quando o build ficar vermelho, você consiga olhar a falha e dizer em dez segundos: “ah, é isso”. Um teste ruim falha e te deixa coçando a cabeça. Um teste bom aponta o dedo para o problema.

Esta nota é a anatomia. Quais ossos sustentam um teste que vale a pena ter.

A receita de bolo: Arrange-Act-Assert

Pense em fazer um bolo. Primeiro você separa os ingredientes na bancada: farinha, ovos, açúcar. Depois você mistura e leva ao forno. Por fim, você espeta o palito para ver se assou.

Todo teste decente segue exatamente essa coreografia. Ela tem nome: AAA — Arrange, Act, Assert (Arranjar, Agir, Verificar).

  • Arrange — monte o cenário. Crie os objetos, prepare os dados, configure os dublês. É a bancada com os ingredientes.
  • Act — execute a única ação que está sob teste. Chame o método. Uma linha, idealmente.
  • Assert — verifique se o resultado bate com o esperado. É o palito no bolo.

O padrão nasceu com Bill Wake (2001) e foi popularizado por Kent Beck em Test Driven Development: By Example.

@Test
void deve_aplicar_desconto_quando_cliente_e_vip() {
    // Arrange
    Cliente cliente = new Cliente(TipoCliente.VIP);
    Carrinho carrinho = new Carrinho(cliente);
    carrinho.adicionar(new Item("Livro", 100.00));
 
    // Act
    BigDecimal total = carrinho.calcularTotal();
 
    // Assert
    assertThat(total).isEqualByComparingTo("90.00"); // 10% off VIP
}

Repare nos três blocos separados por linhas em branco. Você lê de cima a baixo e entende a história sem precisar de comentário (os // Arrange aqui são só didáticos).

flowchart LR
    A["Arrange<br/>monta o cenario"] --> B["Act<br/>executa a acao"]
    B --> C["Assert<br/>verifica o resultado"]
    style A fill:#1e3a5f,color:#fff
    style B fill:#5f4b1e,color:#fff
    style C fill:#1e5f33,color:#fff

Leitura do diagrama: as três fases acontecem em ordem fixa e sem volta. Você nunca arruma de novo no meio do verificar. Se precisar, é sinal de que o teste está fazendo coisa demais.

Por que separar fisicamente

A separação visual das três fases é o que torna o teste legível. Uma linha em branco entre Arrange e Act, outra entre Act e Assert. Quem lê acha o “ponto de virada” sem esforço. É barato e paga muito.

Given-When-Then: o mesmo esqueleto, outro idioma

Se você vier do mundo BDD (Behavior-Driven Development), vai encontrar o mesmo padrão com outro vocabulário: Given-When-Then (Dado-Quando-Então), cunhado por Dan North e Chris Matts em meados dos anos 2000.

  • Given (Dado) = Arrange — o contexto inicial.
  • When (Quando) = Act — o evento que acontece.
  • Then (Então) = Assert — o resultado esperado.

A diferença não é estrutural. É de público. AAA fala a língua do programador (“instancie o objeto”). Given-When-Then fala a língua do stakeholder (“dado um cliente VIP, quando ele finaliza a compra, então recebe 10% de desconto”). É o mesmo bolo descrito para o engenheiro de alimentos ou para quem vai comer.

flowchart TB
    subgraph AAA["AAA — vocabulario tecnico"]
        A1["Arrange"] --> A2["Act"] --> A3["Assert"]
    end
    subgraph GWT["Given-When-Then — vocabulario de negocio"]
        G1["Given"] --> G2["When"] --> G3["Then"]
    end
    A1 -.mesmo papel.- G1
    A2 -.mesmo papel.- G2
    A3 -.mesmo papel.- G3

Leitura do diagrama: as linhas tracejadas dizem o essencial — Given é Arrange, When é Act, Then é Assert. Escolha o vocabulário conforme quem vai ler o teste. Como diz Martin Fowler, pensar em Given-When-Then empurra você a raciocinar sobre comportamento, não sobre estado interno.

Funcionalidade: Desconto VIP
  Cenário: Cliente VIP ganha 10% de desconto
    Dado um cliente do tipo VIP
    E um carrinho com um livro de R$ 100,00
    Quando ele calcula o total
    Então o total deve ser R$ 90,00

O nome conta a história

Aqui mora um dos maiores ganhos de um teste, e o mais ignorado.

O nome do teste deve te dizer o que está sendo testado e sob qual condição — sem você precisar abrir o corpo. Quando o build quebra, o CI te mostra primeiro o nome. Se o nome for test1 ou testCalculo, você não aprendeu nada e vai ter que ler o código todo.

Compare:

  • testDesconto() — inútil. Desconto de quê? Quando? Quanto?
  • deve_aplicar_desconto_de_10_porcento_quando_cliente_e_vip() — você já sabe o que quebrou.

Convenções comuns (escolha uma e seja consistente no projeto):

ConvençãoExemplo
deve_X_quando_Y (PT)deve_lancar_excecao_quando_saldo_insuficiente
should X when Y (EN)should_throw_when_balance_insufficient
methodUnderTest_scenario_expectedBehaviorsaque_saldoInsuficiente_lancaExcecao

Nomes que não dizem nada

test1, testCreate, testHappyPath, testIt. São formas de não nomear. O nome do teste é a primeira linha de documentação que alguém lê quando algo quebra. Trate-o como uma frase, não como um rótulo.

A regra prática: leia o nome em voz alta. Se ele soa como uma frase que descreve uma regra de negócio, está bom. Se soa como o nome de uma variável temporária, refaça.

Um teste, uma razão para falhar

Esta é a propriedade mais sutil — e a que mais separa o júnior do sênior.

Imagine um teste gigante com 20 verificações cobrindo 5 comportamentos diferentes. Ele quebra. O CI mostra “FALHOU”. Qual dos 5 comportamentos está com problema? Você não sabe. Tem que abrir, ler, debugar. O teste falhou, mas não te disse o que falhou.

flowchart TB
    subgraph Gigante["Um teste gigante"]
        T1["testTudo()"] --> C1["comportamento 1"]
        T1 --> C2["comportamento 2"]
        T1 --> C3["comportamento 3"]
        C1 & C2 & C3 --> F1["FALHOU<br/>qual dos 3??"]
    end
    subgraph Focados["Tres testes focados"]
        TA["deve_X..."] --> CA["comportamento 1"] --> FA["passa"]
        TB["deve_Y..."] --> CB["comportamento 2"] --> FB["FALHOU<br/>e o 2"]
        TC["deve_Z..."] --> CC["comportamento 3"] --> FC["passa"]
    end
    style F1 fill:#7a1e1e,color:#fff
    style FB fill:#7a1e1e,color:#fff
    style FA fill:#1e5f33,color:#fff
    style FC fill:#1e5f33,color:#fff

Leitura do diagrama: no teste gigante, a falha é ambígua — sabe-se que algo quebrou, não o quê. Nos testes focados, dois passam (verde) e um falha (vermelho): o diagnóstico vem de graça, sem abrir o código. Cada teste é um fusível: queima o que estourou, deixa o resto aceso.

A regra é: um comportamento por teste. Prefira muitos testes pequenos e focados a um monstro que verifica tudo.

O mito da "uma assertion por teste"

Você vai ouvir “use uma única assertion por teste”. Isso é meia-verdade — vira dogma e atrapalha. O correto é um comportamento por teste, não uma linha de assert por teste. Verificar que um objeto retornado tem nome, email e id corretos pode ser três asserts que validam um comportamento (“o objeto foi montado direito”). Tudo bem. O que você não quer é um teste que verifica cadastro, login e exclusão de uma vez só.

F.I.R.S.T — as cinco leis

Robert C. Martin, no Clean Code, condensou as propriedades de um bom teste unitário em um acrônimo: F.I.R.S.T.

flowchart TB
    FIRST["Um bom teste unitario"]
    FIRST --> F["Fast<br/>roda em milissegundos"]
    FIRST --> I["Independent<br/>nao depende de outro teste"]
    FIRST --> R["Repeatable<br/>mesmo resultado sempre"]
    FIRST --> S["Self-validating<br/>passa ou falha, sem olho humano"]
    FIRST --> T["Timely<br/>escrito junto com o codigo"]
    style F fill:#1e3a5f,color:#fff
    style I fill:#1e3a5f,color:#fff
    style R fill:#1e3a5f,color:#fff
    style S fill:#1e3a5f,color:#fff
    style T fill:#1e3a5f,color:#fff

Leitura do diagrama: as cinco propriedades não são opcionais nem alternativas — um teste unitário decente atende as cinco ao mesmo tempo. Falhar em uma já o degrada.

Fast — rápido

Testes unitários têm que rodar em milissegundos. Por quê? Porque você roda a suíte o tempo todo — a cada salvamento, a cada refactor. Se a suíte leva 30 minutos, você para de rodar. E teste que você não roda não protege ninguém.

Quem deixa o teste lento? Geralmente é banco de dados, rede, disco, sleep. Teste unitário não toca nada disso — quem toca é teste de integração (ver 04 - Testes unitários para a fronteira).

Independent — independente

Cada teste tem que rodar sozinho, em qualquer ordem, sem depender do que outro teste fez antes. Se o teste_B só passa quando o teste_A rodou primeiro (porque A deixou um registro no banco que B usa), você tem um acoplamento escondido. Mude a ordem e tudo desmorona.

Estado compartilhado entre testes é uma das causas-raiz de 11 - Testes flaky — testes que passam às vezes e falham às vezes sem nada ter mudado.

Repeatable — repetível

Rodou agora, rodou daqui a uma hora, rodou na máquina do colega, rodou no CI: mesmo resultado, sempre. Um teste que depende da data atual (new Date()), de um número aleatório, ou de um fuso horário, é uma bomba-relógio. Hoje passa. No dia 31 do mês, ou em outro timezone, falha.

Determinismo é tudo

Repeatable e Independent são primos. Os dois atacam a não-determinismo. Tempo, aleatoriedade, ordem de execução, estado externo — tudo que faz o teste depender de algo que você não controla é inimigo. Veja 11 - Testes flaky para o catálogo completo de causas.

Self-validating — auto-validável

O teste decide sozinho se passou ou falhou. Verde ou vermelho, booleano. Sem você ter que ler um log e julgar “hmm, parece certo”. Se um humano precisa interpretar a saída, não é teste automatizado — é checklist manual disfarçado. A assertion é o juiz; ela diz sim ou não.

Timely — oportuno

Escreva o teste junto com o código de produção, idealmente um pouco antes (a essência do TDD). Por quê “oportuno” e não “qualquer hora”? Porque teste escrito meses depois testa o código que você acha que escreveu, não o que você escreveria se o teste guiasse o design. Teste tardio também tende a nunca ser escrito — sempre tem algo mais urgente.

Sem lógica no teste

Olhe este teste e desconfie:

@Test
void calculaDesconto() {
    for (TipoCliente tipo : TipoCliente.values()) {
        Cliente c = new Cliente(tipo);
        if (tipo == TipoCliente.VIP) {
            assertThat(c.desconto()).isEqualByComparingTo("0.10");
        } else {
            assertThat(c.desconto()).isEqualByComparingTo("0.00");
        }
    }
}

Tem for, tem if. Isso é código. E código tem bug. Quem testa o teste? Se a lógica condicional do teste estiver errada, ele pode passar quando deveria falhar — um falso positivo silencioso.

Pior: lógica no teste quase sempre é sinal de que o teste está fazendo coisa demais. Aquele for está escondendo vários casos diferentes que mereciam ser testes separados (com nomes próprios, e diagnóstico próprio quando falham).

Use testes parametrizados, não loops

JUnit (@ParameterizedTest), pytest (@pytest.mark.parametrize), xUnit ([Theory]) existem exatamente para isso: rodar o mesmo teste com vários dados, sem você escrever o loop. Cada caso vira uma linha de dados, o framework cuida da iteração, e cada caso falha individualmente com nome próprio.

A regra: o corpo de um teste deve ser uma linha reta. Arrange, Act, Assert, fim. Sem desvios.

O teste como documentação viva

README envelhece. Comentário mente. Mas o teste, se está verde, está dizendo a verdade sobre o que o código faz — porque ele roda contra o código.

Uma classe de teste bem nomeada é a melhor documentação executável que existe:

ClienteVipTest
  ✓ deve_aplicar_10_porcento_de_desconto_no_total
  ✓ deve_acumular_pontos_em_dobro
  ✗ deve_ter_frete_gratis_acima_de_50_reais

Lendo só os nomes, um dev novo entende as regras de negócio do cliente VIP em 15 segundos — e ainda vê que o frete grátis está quebrado. Nenhum README faz isso.

Por isso, legibilidade vence esperteza. Um teste que usa um truque genial mas exige cinco minutos para ser entendido é um teste ruim, mesmo que correto. Escreva o teste para o humano que vai lê-lo às 3 da manhã durante um incidente, não para impressionar.

Comportamento, não implementação

A propriedade mais importante de um bom teste — testar o que o código faz, não como ele faz — é tão central que tem nota dedicada. Um teste amarrado aos detalhes internos quebra a cada refactor mesmo quando o comportamento não mudou. Aprofunde em 06 - Testar comportamento, não implementação.

Em entrevista

A good test is structured as Arrange-Act-Assert — set up the scenario, perform one action, verify the outcome — which is the same skeleton as Given-When-Then in BDD, just spoken in stakeholder language. The test name should tell you what and when without reading the body, using a convention like should_X_when_Y or methodUnderTest_scenario_expectedBehavior. I aim for one reason to fail per test: many small focused tests beat one giant test, because a focused test that fails points straight at the broken behavior. I keep tests F.I.R.S.T — Fast, Independent, Repeatable, Self-validating, Timely — and I avoid conditional logic (if/for) inside tests, reaching for parameterized tests instead. Well-named tests are the best living documentation: they can’t go stale because they run against the code. And the deepest property is testing behavior, not implementation, so the suite survives refactoring.

Vocabulário

PTEN
arranjo-ação-verificaçãoarrange-act-assert
dado-quando-entãogiven-when-then
auto-validávelself-validating
uma razão para falharone reason to fail
testes parametrizadosparameterized tests
documentação vivaliving documentation
falso positivofalse positive
convenção de nomenclaturanaming convention
comportamento, não implementaçãobehavior, not implementation

Lastro

  • Martin Fowler, “Given When Then” — origem do GWT com Dan North e Chris Matts; raciocinar em comportamento, não estado.
  • Bill Wake / Kent Beck, Test Driven Development: By Example (2002) — onde o idioma Arrange-Act-Assert foi popularizado.
  • Robert C. Martin, Clean Code (cap. “Unit Tests”) — origem do acrônimo F.I.R.S.T (Fast, Independent, Repeatable, Self-validating, Timely).

Veja também