Erros comuns em TDD que você deve evitar
Guia técnico, direto ao ponto: como manter o ciclo Red-Green-Refactor fiel, com foco em qualidade, legibilidade e manutenibilidade.
1) Não seguir o ciclo Red-Green-Refactor: o erro mais comum de quem está aprendendo TDD
O TDD prega o ciclo explícito: escrever um teste que falha (vermelho), criar a mínima implementação que passe (verde) e, por fim, refatorar sem quebrar o comportamento. O problema mais frequente é pular uma dessas etapas ou encurtá-las sem verificação adequada.
- Escrever código de produção para “apenas fazer o teste passar” sem ver o que o teste verdadeiramente exige.
- Ignorar o estado de falha intencional do teste e já tentar adaptar a API antes de entender o requisito.
- Refatorar código e testes simultaneamente sem validação incremental, gerando regressões sutis.
Boas práticas rápidas:
2) Testes fracos ou acoplados à implementação
Testes que dependem de detalhes da implementação (nomenclatura de métodos, estruturas internas ou classes privadas) se tornam frágeis, dificultando refatorações e desencorajando mudanças no código de produção.
Priorize testar o comportamento público do código, via API exposta, e use mocks apenas para dependências externas. Evite testar detalhes internos que podem mudar sem impactar o contrato com o consumidor.
Exemplo prático
Observação: o objetivo é demonstrar a diferença entre testar comportamento vs. implementação.
// Mau exemplo: teste que depende de implementação interna
// Descrever o que o objeto "Calculadora" faz internamente
describe("Calculadora (teste de implementação)", () => {
it("deveria utilizar o método privado _formatResult", () => {
const calc = new Calculadora();
// @ts-ignore
const formatted = calc._formatResult(5);
expect(formatted).toBe("5.00");
});
});
// Bom exemplo: testa apenas o comportamento público
describe("Calculadora (teste de comportamento)", () => {
it("deveria formatar o resultado pelo método público format()", () => {
const calc = new Calculadora();
expect(calc.format(5)).toBe("5.00");
});
});
Princípio aplicado: os testes devem validar o que o usuário final observa (contrato público), não como o código está implementado por trás das cenas.
3) Falta de isolamento de dependências externas
Uma armadilha comum em TDD é depender de recursos externos (banco de dados, rede, relógio do sistema) nos testes. Isso gera flakiness, lentidão e dificuldade de reproduzir falhas localmente.
- Quando possível, injete dependências para permitir substituição por fakes/mocks durante o teste.
- Frozen clocks para cenários de tempo determinístico; use técnicas de “time freezing” apenas quando necessário, sem comprometer o contrato de tempo do código.
- Prefira testes de unidade com mocks de dependências, reservando testes de integração para validação de integração entre módulos críticos.
Dicas rápidas:
- Avalie se cada teste realmente isola o comportamento desejado.
- Considere usar fábricas de objetos de teste para criar cenários repetíveis sem depender de dados mágicos.
4) Débito de refatoração nos testes e duplicação de código
Testes mal estruturados costumam acumular duplicação de setup, asserts repetidos e dados de teste mágicos. Isso dificulta manutenção, especialmente quando o domínio evolui.
- Utilize setup comum com beforeEach/afterEach para reduzir duplicação sem esconder a intenção de cada teste.
- Construa dados de teste com builders/factories em vez de literais espalhados pelo suite.
- Nomeie testes de forma explícita para refletir o comportamento esperado, não a implementação do código.
Boas práticas sugeridas:
- Extraia helpers de teste para cenários recorrentes e documente o que cada helper representa.
- Refatore testes junto com o código de produção, sempre verificando que a funcionalidade permanece intacta.
Exemplo de construção de dados de teste (test data builder)
// Test Data Builder em JavaScript/TypeScript
class CartBuilder {
constructor() {
this.items = [];
this.taxRate = 0.0;
}
withItem(name, price, qty = 1) {
this.items.push({ name, price, qty });
return this;
}
withTaxRate(rate) {
this.taxRate = rate;
return this;
}
build() {
return new Cart(this.items, this.taxRate);
}
}
// uso no teste
describe("Cart", () => {
it("deve calcular total com imposto", () => {
const cart = new CartBuilder()
.withItem("Camiseta", 50, 2)
.withTaxRate(0.1)
.build();
expect(cart.total()).toBeCloseTo(110);
});
});
Gostou do conteúdo? Aprofunde-se em mais artigos técnicos sobre qualidade de código, padrões de teste e estratégias de desenvolvimento orientado a testes. Leia outros posts no Yurideveloper para aumentar sua didática prática em TDD e design de software.