O grande mal-entendido sobre TDD (2022)

Test-driven development (TDD) e unit testing são examinados aqui, com muitos argumentando que testes fortemente acoplados aos detalhes de implementação criam suítes frágeis que dificultam refatorações e desperdiçam esforço. Os comentaristas contrapõem testes unitários de baixo nível a testes de integração e end-to-end de nível mais alto, debatendo onde está o melhor custo-benefício, quanta cobertura é realmente útil e se os testes devem, acima de tudo, validar o comportamento a partir de uma fronteira de usuário ou de sistema. Por trás disso, há uma preocupação mais ampla com metodologias dogmáticas: alguns veem o TDD como um poderoso auxílio ao design quando aplicado com cuidado, enquanto outros o tratam como uma ferramenta situacional que deve ser escolhida com base no contexto, nos objetivos de qualidade e na complexidade do sistema.

Significado de “unit” em unit testing

  • A afirmação do artigo de que “unit” originalmente se referia a um teste isolado é fortemente contestada.
  • Vários comentaristas citam definições anteriores (por exemplo, “small compilable module, ~100 LOC”) em que a unidade é claramente o código sob teste, não o teste.
  • O consenso no tópico: a linguagem derivou, mas a maioria dos praticantes e documentos históricos trata a unidade como um componente de código (função, classe, módulo), embora alguns aceitem “independent test” como uma noção moderna útil.

TDD é sobre testing ou design?

  • Um grupo: o grande mal-entendido é tratar TDD como testing; ele é principalmente uma prática de design que usa testes para moldar APIs e a separação de responsabilidades.
  • Grupo oposto: TDD-como-design é superestimado ou prejudicial; habilidade e julgamento de design importam mais, e o TDD oferece orientação de design fraca, especialmente para devs menos experientes.
  • Alguns dividem o termo: Test-Driven Development vs Test-Driven Design, argumentando que eles são frequentemente confundidos.

Unit vs integration/E2E tests

  • Muitos argumentam que testes funcionais/de integração de alto nível oferecem melhor valor:
    • Mais próximos do comportamento visível ao usuário.
    • Mais estáveis durante refatorações.
    • Evitam o acoplamento excessivo dos testes à implementação e a proliferação de mocks.
  • Outros defendem a pirâmide de testes tradicional: numerosos testes unitários rápidos e isolados capturam casos extremos e bugs que testes mais amplos podem deixar passar, especialmente para algoritmos complexos ou bibliotecas.
  • Compromisso comum:
    • Testes unitários para lógica pura e complexa ou bases reutilizáveis.
    • Testes de integração/E2E para fluxos de negócio e fronteiras HTTP/JSON ou de UI.

Testes frágeis e dor na refatoração

  • Reclamação frequente: refatorar “sem mudar o comportamento” quebra muitos testes de baixo nível.
  • Diagnósticos: os testes são excessivamente white-box, fazem over-mocking ou afirmam detalhes internos irrelevantes.
  • Estratégias mencionadas:
    • Focar os testes no comportamento observável externamente.
    • Aceitar que alguns testes iniciais, focados na implementação, devem ser removidos ou evoluir quando testes de comportamento de nível mais alto existirem.
    • Usar testes mais como um suporte de segurança para refatoração do que como restrições de design.

Dogmatismo, fluxo de trabalho e métricas

  • Debate sobre “nunca mudar código sem um teste vermelho”: alguns veem isso como central para TDD, outros como ritual desnecessário, especialmente durante trabalho exploratório.
  • Ceticismo amplo sobre TDD de culto, cobertura obrigatória de 100% e coverage como KPI (lei de Goodhart).
  • Vários enfatizam contexto: risco do domínio, linguagem (dinâmica vs fortemente tipada), requisitos não funcionais e habilidade da equipe devem orientar a estratégia de testes, não uma única metodologia universal.