Yuri PeixinhoIntrodução TDD (Test-Driven Development, ou Desenvolvimento Guiado por Testes) inverte a...
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 é 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 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:
!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.
Robert C. Martin (Uncle Bob) resumiu a disciplina do TDD em três leis, aplicadas em conjunto e repetidamente:
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);
}
// 2. Código mínimo para passar (Green)
public class CalculadoraDeDesconto
{
public decimal Calcular(decimal valorTotal) => 0m;
}
// 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);
}
// 4. Agora o comportamento real aparece (Green)
public class CalculadoraDeDesconto
{
public decimal Calcular(decimal valorTotal) =>
valorTotal > 100m ? valorTotal * 0.10m : 0m;
}
Repare: o código de produção só cresceu porque um teste exigiu. Nenhuma regra de desconto foi antecipada "porque provavelmente vai precisar".
Um teste bem escrito segue uma estrutura previsível, descrita pelo acrônimo AAA:
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);
}
Boas práticas de nomenclatura e estrutura:
Metodo_Cenario_ResultadoEsperado (ex.: Reservar_QuantidadeMaiorQueEstoque_LancaExcecao). Quem lê o nome do teste falhando já entende o que quebrou, sem abrir o código.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.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.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:
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.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.