Testing

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

Testes documentam o comportamento esperado. Um teste que falha conta uma história: quem chamou, o que recebeu, o que esperava.

Conceitos fundamentais

ConceitoO que é
AAA (Arrange, Act, Assert · Arranjar, Agir, Atestar)Estrutura de três fases para cada teste: montagem do contexto, execução e verificação do resultado
Unit test (teste unitário)Teste de uma função em isolamento, sem I/O real; dependências externas são substituídas por doubles
Integration test (teste de integração)Teste com infraestrutura real (banco, rede, fila) para verificar que os componentes funcionam juntos
Doubles (substitutos de teste)Substitutos de dependências externas: stubs (retornam valor fixo), mocks (verificam interação), fakes (implementação simplificada)
Fixture (dado de teste pré-definido)Contexto ou dado passado para configurar o estado do teste
cyclomatic complexity (complexidade ciclomática)Número de caminhos independentes em uma função; equivale ao mínimo de casos de teste necessários para cobertura de ramificações

AAA

O padrão AAA (Arrange, Act, Assert · Arranjar, Agir, Atestar) divide cada teste em três fases explícitas, separadas por uma linha em branco.

[Arrange]  montar o contexto: entidades, inputs, dependências

[Act]      executar o comportamento sob teste

[Assert]   verificar o resultado com variáveis nomeadas

A separação visual é parte do padrão. Cada linha em branco entre fases é intencional: sinaliza onde termina o contexto, onde acontece a execução e onde está a verificação. Um teste sem separação mistura as três fases e obriga o leitor a identificar os limites antes de entender o que está sendo testado.

No logic no assert

O assert segue o mesmo princípio do retorno explicativo no código: variáveis nomeadas antes da comparação. O assert lê como uma frase, sem cálculo nem acesso de propriedade inline.

Anti-patternProblema
assert(calculate(100, 10), 90)Literal inline: a falha não diz o que era esperado
assert(result.price, getExpected())Lógica na comparação: dois pontos de falha simultâneos
assert(result.items.length, 3)Acesso de propriedade inline: a falha mostra o caminho, não o valor

actual e expected são declarados antes do assert, sempre. Mesmo quando o valor já tem nome, declarar expected explicitamente mantém o padrão consistente e o assert sem ambiguidade.

Nome do teste

O nome descreve o cenário e o resultado esperado. Prefixos como should, test_, given/when/then não agregam informação.

EvitarUsar
should apply discountapplies 10% discount when order exceeds minimum
test validationthrows ValidationError when discount is negative
applyDiscount functionreturns original price when no discount applies

Isolamento

Cada teste monta seu próprio contexto. Nenhum teste depende de outro para funcionar: a ordem de execução não deve importar. Estado compartilhado entre testes é o tipo de acoplamento mais silencioso, pois falhas dependem da ordem de execução e são difíceis de reproduzir isoladamente.

Testes Unitários

Verificam o comportamento de uma unidade isolada: uma função, um método, uma classe. Sem banco, sem rede, sem sistema de arquivos. Dependências externas são substituídas por doubles (stubs, mocks, fakes) quando necessário.

Velocidade é a característica central: rodam em milissegundos, sem setup de infraestrutura. Isso os torna adequados para feedback rápido durante o desenvolvimento e cobertura exaustiva de casos de borda.

CaracterísticaDetalhe
VelocidadeAlta: sem I/O (Entrada/Saída)
IsolamentoTotal: dependências externas substituídas
Cobertura naturalRegras de negócio, transformações, validações, cálculos
Quando falhaIndica bug na lógica da unidade testada

Testes de Integração

Verificam como múltiplos componentes funcionam juntos, com infraestrutura real: banco de dados, APIs externas, filas, sistema de arquivos.

O custo é o setup: precisam de ambiente, são mais lentos e têm mais variáveis. O benefício é verificar o que testes unitários não alcançam: que a SQL (Structured Query Language, Linguagem de Consulta Estruturada) gerada está correta, que a migration não quebrou o mapeamento, que o endpoint retorna o contrato esperado.

CaracterísticaDetalhe
VelocidadeBaixa: com I/O real
IsolamentoParcial: envolve infraestrutura
Cobertura naturalFronteiras entre componentes e sistemas externos
Quando falhaIndica problema na integração, não na lógica isolada

Um erro frequente é usar mocks extensivos para simular o banco em testes "unitários" de repositório. O repositório existe para falar com o banco: testá-lo com banco falso verifica quase nada. Repositórios são candidatos naturais a testes de integração.

Unitário ou integração?

A distinção é o que cada tipo verifica. Testes unitários verificam se a lógica está correta. Testes de integração verificam se os componentes funcionam juntos com infraestrutura real. Os dois são necessários e se complementam.

lógica isolada (funções, cálculos, validações) → unitário
fronteira com I/O real (banco, rede, fila)     → integração
CenárioTipo certo
Regra de desconto com múltiplos casos de bordaUnitário
Query SQL retorna os registros corretosIntegração
Validação de entrada com 10 casos inválidosUnitário
Endpoint cria pedido e persiste no bancoIntegração
Cálculo de frete por CEP e pesoUnitário
Worker de fila processa mensagem completaIntegração

Complexidade ciclomática

Complexidade ciclomática (cyclomatic complexity) mede o número de caminhos independentes em uma função. A complexidade mínima é 1 (linha reta); cada if, else if, case, loop, &&, || e catch soma +1.

FaixaAvaliação
1–10Simples; fácil de testar e manter
11–20Moderada; requer atenção
21–50Alta; difícil de testar; candidato a refatoração
> 50Intratável; cobertura completa é inviável na prática

A métrica tem uma consequência direta nos testes: uma função com complexidade N exige pelo menos N casos de teste para cobertura de branch (cobertura de ramificações). Funções com complexidade alta concentram risco: uma mudança pequena pode quebrar múltiplos caminhos.

Quando a complexidade supera 10, as ações são as mesmas que para funções longas:

  • extrair lógica condicional em funções menores com responsabilidade única
  • substituir switch extenso por tabela de despacho (dispatch table) ou padrão Strategy
  • usar guard clauses para eliminar aninhamento

Por linguagem

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.