Test Driven Development (TDD)

# programming# softwaredevelopment# testing
Test Driven Development (TDD)Yuri Peixinho

Introdução TDD (Test-Driven Development, ou Desenvolvimento Guiado por Testes) inverte a...

Introdução

TDD (Test-Driven Development, ou Desenvolvimento Guiado por Testes) inverte a ordem natural de escrever código: em vez de implementar a funcionalidade e só depois escrever testes para confirmar que ela funciona, você escreve o teste primeiro — um teste que falha, porque o código que ele exercita ainda nem existe — e só então escreve o código mínimo necessário para fazer esse teste passar.

Essa inversão parece pequena, mas muda tudo. Quando você testa depois, o teste normalmente confirma o que o código já faz — inclusive os vícios de design que ele carrega. Quando você testa antes, o teste é quem dita como o código deveria ser usado: sua API, suas dependências, seus pontos de extensão. O teste vira uma especificação executável, não uma rede de segurança criada depois do fato.

TDD não é sobre "ter testes". É sobre usar o ato de escrever testes como ferramenta de design. Um código difícil de testar quase sempre é um código com problemas de design: acoplamento alto, responsabilidades misturadas, dependências escondidas. TDD expõe esses problemas cedo, antes que fiquem caros de corrigir.
Este artigo percorre o caminho completo: o ciclo básico, as três leis formais, a anatomia de um bom teste, um exemplo prático do zero, os tipos de dublê de teste, a pirâmide de testes, como TDD molda a arquitetura, como aplicar tudo isso na stack deste projeto (C#, xUnit, FluentAssertions, NSubstitute), as duas escolas de pensamento, as armadilhas mais comuns e como resgatar código legado sem rede de segurança. Ao final, um checklist prático e uma lista de recursos para continuar evoluindo.

TDD é uma arquitetura?

TDD é um processo, não uma arquitetura. É uma disciplina de como você escreve código (teste antes, código mínimo, refatora), não uma definição de como o sistema é estruturado (camadas, módulos, quem depende de quem). Você pode praticar TDD dentro de qualquer arquitetura, inclusive uma ruim.

Arquitetura é uma preocupação separada: é o conjunto de decisões estruturais: Clean Architecture, Hexagonal (Ports & Adapters), camadas, microsserviços, etc, que definem fronteiras e direção de dependência.

A confusão existe porque TDD empurra a arquitetura numa certa direção como efeito colateral: para um teste ser fácil de escrever, a unidade sob teste precisa ter dependências injetáveis e abstraídas. Isso é exatamente o que arquiteturas como Clean/Hexagonal formalizam de propósito. Não é coincidência que a separação deste projeto (Core.Domain → Core.Application → Core.Infrastructure, com a camada de aplicação dependendo de interfaces, não de implementações concretas) seja tão amigável a TDD: é uma arquitetura que já nasce com testabilidade como requisito de design.

Podemos dizer então que TDD não escolhe a arquitetura, mas pressiona você a ter uma onde cada peça seja testável isoladamente e isso, na prática, costuma convergir para os mesmos princípios que arquiteturas como Clean Architecture defendem (baixo acoplamento, inversão de dependência, fronteiras explícitas).

O ciclo Red-Green-Refactor

O TDD funciona em um ciclo curto que se repete dezenas de vezes por dia: Red (vermelho), Green (verde) e Refactor (refatorar).

Red: Escreva um teste para um comportamento que ainda não existe, e rode a suíte. O teste deve falhar, se ele passar de cara, ou o comportamento já existe, ou o teste não está testando nada. Falhar aqui é o objetivo: é a prova de que o teste é capaz de detectar a ausência do comportamento.

Green: Escreva o código de produção mais simples possível para fazer o teste passar. Simples mesmo: é aceitável, e recomendado no início, retornar um valor fixo se isso for suficiente para passar o teste atual. A elegância vem depois; agora o objetivo é sair do vermelho o mais rápido possível.

Refactor: Com o teste passando, limpe o código: remova duplicação, melhore nomes, extraia métodos, ajuste o design. Os testes que já passam são sua rede de segurança: qualquer regressão introduzida na refatoração é pega imediatamente. Nenhum comportamento novo é adicionado nesta fase, só melhoria estrutural.

Depois do refactor, o ciclo recomeça com um novo teste Red. Cada volta do ciclo deve durar minutos, não horas: passos pequenos demais parecem lentos no início, mas evitam o cenário clássico de "quebrei tudo e não sei por quê".

Erros comuns de quem está começando:

  • Escrever vários testes de uma vez antes de fazer qualquer um passar, perdendo o feedback imediato do ciclo.
  • Pular a fase Red, escrevendo o código de produção primeiro e o teste depois só para "confirmar" — isso não é TDD, é teste depois do fato.
  • Pular o Refactor porque "já está funcionando" — é assim que o código acumula dívida técnica mesmo com testes.
  • Escrever uma solução completa e genérica já na fase Green, em vez da solução mínima — isso tende a acoplar o design a suposições erradas.

!image.png

O loop fecha de Refactor de volta para Red: a cada volta, um novo teste força o próximo incremento de comportamento.

As três leis do TDD

Robert C. Martin (Uncle Bob) resumiu a disciplina do TDD em três leis, aplicadas em conjunto e repetidamente:

  1. Você não pode escrever código de produção até escrever um teste que falhe.
  2. Você não pode escrever mais de um teste do que o suficiente para falhar (não compilar também conta como falhar).
  3. Você não pode escrever mais código de produção do que o necessário para fazer o teste atual passar.

Seguidas à risca, essas leis forçam ciclos de segundos: escreva uma linha de teste, veja falhar, escreva uma linha de produção, veja passar, repita. Na prática, poucos times seguem isso de forma tão granular — mas entender a disciplina no extremo ajuda a reconhecer quando você se afastou dela (por exemplo, ao escrever 40 linhas de produção para um teste que ainda nem rodou).

Exemplo do ritmo em C#, implementando uma calculadora de desconto:

// 1. Teste que falha (Red) — ainda não existe CalculadoraDeDesconto
[Fact]
public void SemItens_DevolveDescontoZero()
{
    var calculadora = new CalculadoraDeDesconto();

    var desconto = calculadora.Calcular(valorTotal: 0);

    desconto.Should().Be(0m);
}
Enter fullscreen mode Exit fullscreen mode
// 2. Código mínimo para passar (Green)
public class CalculadoraDeDesconto
{
    public decimal Calcular(decimal valorTotal) => 0m;
}
Enter fullscreen mode Exit fullscreen mode
// 3. Novo teste força generalização (Red de novo)
[Fact]
public void AcimaDeCemReais_AplicaDezPorCentoDeDesconto()
{
    var calculadora = new CalculadoraDeDesconto();

    var desconto = calculadora.Calcular(valorTotal: 200m);

    desconto.Should().Be(20m);
}
Enter fullscreen mode Exit fullscreen mode
// 4. Agora o comportamento real aparece (Green)
public class CalculadoraDeDesconto
{
    public decimal Calcular(decimal valorTotal) =>
        valorTotal > 100m ? valorTotal * 0.10m : 0m;
}
Enter fullscreen mode Exit fullscreen mode

Repare: o código de produção só cresceu porque um teste exigiu. Nenhuma regra de desconto foi antecipada "porque provavelmente vai precisar".

3. Anatomia de um bom teste

Um teste bem escrito segue uma estrutura previsível, descrita pelo acrônimo AAA:

  • Arrange (organizar): prepara o cenário — cria objetos, configura dependências, define valores de entrada.
  • Act (agir): executa a ação testada — normalmente uma única chamada de método.
  • Assert (verificar): confirma que o resultado observado é o esperado.

Times que praticam BDD usam a variação Given-When-Then, com o mesmo espírito: Given (dado um contexto) corresponde ao Arrange, When (quando uma ação ocorre) ao Act, Then (então um resultado é esperado) ao Assert. A diferença é mais de vocabulário e foco em linguagem de negócio do que de mecânica.

[Fact]
public void Pedido_ComItemAbaixoDoEstoque_ReduzEstoqueDisponivel()
{
    // Arrange (Given)
    var estoque = new Estoque(quantidadeDisponivel: 10);

    // Act (When)
    estoque.Reservar(quantidade: 3);

    // Assert (Then)
    estoque.QuantidadeDisponivel.Should().Be(7);
}
Enter fullscreen mode Exit fullscreen mode

Boas práticas de nomenclatura e estrutura:

  • Nome do teste conta uma história: Metodo_Cenario_ResultadoEsperado (ex.: Reservar_QuantidadeMaiorQueEstoque_LancaExcecao). Quem lê o nome do teste falhando já entende o que quebrou, sem abrir o código.
  • Um teste, um motivo para falhar: evite múltiplos Assert verificando coisas não relacionadas no mesmo teste. Vários asserts sobre o MESMO resultado (vários campos de um mesmo objeto) são aceitáveis.
  • Sem lógica condicional no teste: if, for, switch dentro de um teste são sinal de alerta — um teste deve ser trivial de ler, sem precisar ser depurado para entender o que ele verifica.
  • Independência: um teste não pode depender da ordem de execução nem do estado deixado por outro teste. Cada teste prepara seu próprio cenário do zero.

TDD molda o design

TDD não é só sobre confiança — é sobre forçar boas decisões de design antes que o código endureça. Um teste unitário só é fácil de escrever quando a unidade sob teste tem poucas dependências, e essas dependências são visíveis (injetadas), não escondidas (instanciadas internamente ou acessadas via estáticos/singletons globais).

Esse efeito colateral é intencional e muito valioso:

  • Baixo acoplamento: se para testar uma classe você precisa configurar meia aplicação, o teste "dói" — e essa dor é um sinal de design, não um problema do framework de testes. TDD expõe isso na primeira tentativa de escrever o teste, antes que o acoplamento se espalhe para mais código.
  • Injeção de dependência: para substituir uma dependência por um dublê de teste, ela precisa ser injetável (via construtor, geralmente) em vez de criada com new dentro do método. Escrever o teste primeiro empurra você a programar contra abstrações desde o início — o "D" do SOLID (Dependency Inversion) emerge naturalmente.
  • Interfaces pequenas e coesas: é mais fácil criar um dublê para uma interface com 2 métodos do que para uma com 20. TDD incentiva interfaces enxutas — o "I" do SOLID (Interface Segregation).
  • Responsabilidade única: uma classe que faz muitas coisas gera testes longos, com muito Arrange, testando cenários não relacionados. Quando os testes ficam difíceis de nomear ou de entender, geralmente é sinal de que a classe sob teste deveria ser duas.

Se você está lutando para testar algo, antes de recorrer a mocks cada vez mais elaborados, pergunte se o design não está pedindo para ser simplificado. Testabilidade e boas práticas de design andam juntas.