MÓDULO 02 · CONCEITO 01 DE 8

TDD em profundidade

O ciclo red/green/refactor destrinchado — disciplina específica de cada passo e os erros típicos de quem está aprendendo.

Tempo de leitura ~24 min Pré-requisito Módulo 00 (testabilidade) Próximo Pirâmide, Troféu & Honeycomb

No Módulo 00, vimos testabilidade como propriedade de design — código difícil de testar é código com fronteiras mal-postas. Aqui mergulhamos no método que torna isso operacional: Test-Driven Development. Kent Beck publicou Test-Driven Development: By Example em 2002, mas a prática vinha sendo usada há anos antes — começou no projeto C3 da Chrysler em meados dos anos 90, junto com a invenção da Extreme Programming. O que parecia excentricidade naquele momento virou prática mainstream da indústria, embora ainda mal-compreendida.

A confusão típica é tratar TDD como sinônimo de "ter testes". São coisas distintas. Você pode ter excelente cobertura de testes sem nunca ter praticado TDD; pode praticar TDD e produzir código com testes ruins se pular o refactor. TDD é o ciclo: red, green, refactor — nessa ordem, com disciplina específica em cada fase. Quando o ciclo é seguido com integridade, o efeito sobre o design é o ponto principal — não o teste em si. Ter testes é resultado; design melhor é a causa.

O ciclo, com detalhe

Red — escrever um teste que falha

O primeiro passo parece banal mas concentra a maior parte do valor cognitivo do método. Você escreve um teste para um comportamento que ainda não existe. O teste falha (red). Esse momento exige clareza surpreendente: para escrever o teste, você precisa decidir, em código executável:

Cada uma dessas decisões é uma decisão de design. Estão sendo tomadas antes de escrever a implementação. Esse é o ponto: forçar o design a vir primeiro, em forma executável, em vez de emergir acidentalmente da implementação. Quando você tenta escrever esse teste e sente que a API não está clara, você descobriu o problema antes de gastar uma hora implementando-o.

A disciplina aqui é resistir à urgência de codar. Quando você já sabe o que vai escrever, parece desperdício passar pelo ritual do teste primeiro. Não é. O ritual é onde você verifica se realmente sabe. Frequentemente descobre que não sabia tão bem quanto pensava — e essa descoberta no teste é muito mais barata do que na implementação.

Green — fazer o teste passar com o mínimo

Com o teste vermelho diante de você, escreva a menor quantidade de código possível para torná-lo verde. "Mínimo" aqui é literal. Se o teste espera add(2, 3) == 5, retornar 5 hardcoded é uma resposta válida. Beck chama isso de "fake it till you make it" — você pode literalmente devolver constantes na primeira iteração.

Isso parece absurdo até você ver o porquê. Implementar pouco é uma forma de garantir que o que você implementa é exigido por um teste. Toda linha de código deveria existir porque algum teste a requer; código não-coberto é código suspeito de ser desnecessário. Implementar a lógica completa na primeira iteração é fazer hipóteses sobre o que vai ser pedido depois — e hipóteses costumam ser erradas. TDD substitui hipóteses por evidência: implementa só o pedido, e quando vem novo pedido (próximo teste), aí generaliza.

Esse processo é chamado de triangulação. Primeiro teste passa com hardcoded. Segundo teste com inputs diferentes força você a generalizar — não pode mais retornar constante. Terceiro teste pode forçar mais generalização. A solução geral emerge da pressão dos casos específicos, em vez de ser pré-concebida. Para problemas onde a generalização correta não é óbvia, isso é tremendo — você não cai em abstrações erradas porque não as construiu até serem necessárias.

Refactor — melhorar o design com testes verdes

Esta é a fase mais negligenciada e a que dá mais retorno. Com testes verdes, você está numa zona de segurança: pode mudar o código quanto quiser; se tudo continua verde, você não quebrou nada. Use isso para melhorar o design.

O que melhorar:

Pular o refactor é o erro mais comum de quem está aprendendo TDD. O resultado é código que passa em testes mas é tão bagunçado quanto código escrito sem método. Fica a cobertura, perde-se o ganho de design. Comprometa-se com o refactor — mesmo quando "o teste já passou e estou cansada".

heurística do sênior

Se você refatorou e algum teste quebrou, pare. Esse teste estava verificando implementação, não comportamento. Refatoração não deveria quebrar testes — se quebrou, o teste é frágil e merece ser repensado. Frequentemente é caso de mock excessivo (testando como, não o que).

Os três níveis de Beck — baby steps

Quando alguém pergunta a Beck "que tamanho deve ter o passo entre testes?", a resposta canônica é: depende da sua confiança. Beck propõe três níveis de cautela:

  1. Implementação óbvia: você sabe exatamente o que escrever. Vai e escreve. Próximo teste.
  2. Triangulação: você não tem certeza da generalização. Faz hardcoded; segundo teste força generalização parcial; terceiro teste força a final.
  3. Fake it: você nem sabe por onde começar. Faz hardcoded para passar; aos poucos refatora a constante para algo mais geral.

A escolha não é dogmática — é função da sua confiança no momento. Para problemas familiares, "implementação óbvia" é mais rápida. Para problemas novos, baby steps com triangulação evitam que você se perca. Quando cansado ou em código complexo, fake it pode ser a única forma de avançar.

Outside-in vs Inside-out

Há duas escolas dentro do TDD sobre onde começar. A diferença é sutil mas tem implicações práticas grandes.

Inside-out (clássica, "Detroit", "Chicago"): comece pelas classes mais internas, com o domínio. Construa peças pequenas, bem testadas, e vá compondo até a fronteira. Os testes são predominantemente sobre estado (verifica retorno, verifica mudança de estado). Foi o estilo original de Beck.

Outside-in ("London", mockist): comece pela fronteira (controlador, caso de uso) e desça. Use mocks para colaboradores que ainda não existem; depois implemente esses colaboradores. Os testes são predominantemente sobre interação (verifica que método foi chamado). Codificada em Growing Object-Oriented Software, Guided by Tests de Steve Freeman e Nat Pryce.

Cada escola tem trade-offs:

A maioria dos praticantes maduros mistura: outside-in para descobrir a fronteira; inside-out para o núcleo de domínio. Não há pureza necessária — o método é meio, não fim.

Os katas clássicos — onde se aprende

Aprender TDD via projeto real é difícil. Você está distraído por requisitos ambíguos, deadlines, integração com tudo. Os katas são exercícios curtos, com problema bem-definido, focados em treinar o ciclo. Repetir o mesmo kata várias vezes — sim, sabendo a resposta — internaliza o ritmo.

Os mais valiosos:

A regra do kata é: faça pelo menos três vezes. Primeira vez, você aprende o problema. Segunda, aprende o método aplicado a ele. Terceira, foca na qualidade do ritmo — pequenos passos, ciclo curto. Codercise.com e kata-log.rocks têm catálogos extensos.

O ciclo na prática — String Calculator

Vamos exercitar o ciclo num exemplo concreto: implementar add(numbers: string) -> int que recebe string com números separados por vírgula e retorna soma. Em incrementos, vou mostrar o ciclo.

Iteração 1: add("") deve retornar 0.

// Red
public void Add_EmptyString_Returns0() {
    Calculator.Add("").Should().Be(0);
}

// Green (mínimo!)
public static int Add(string s) => 0;

Iteração 2: add("5") deve retornar 5.

// Red — adicionar
public void Add_OneNumber_ReturnsThatNumber() {
    Calculator.Add("5").Should().Be(5);
}

// Green — agora não pode mais retornar 0
public static int Add(string s) =>
    string.IsNullOrEmpty(s) ? 0 : int.Parse(s);

Iteração 3: add("2,3") deve retornar 5.

// Red
public void Add_TwoNumbers_ReturnsSum() {
    Calculator.Add("2,3").Should().Be(5);
}

// Green
public static int Add(string s) {
    if (string.IsNullOrEmpty(s)) return 0;
    return s.Split(',').Sum(int.Parse);
}

Refactor: a expressão string.IsNullOrEmpty(s) ? 0 : ... é redundante agora — "".Split(',') retorna [""] que pode ser tratado se substituirmos a parse direto:

public static int Add(string s) =>
    s.Split(',', StringSplitOptions.RemoveEmptyEntries)
     .Sum(int.Parse);

Cada iteração tem teste vermelho → verde → opcional refactor. O programa final emergiu de pressão de testes específicos, não de "design upfront". A prática repetida desenvolve o ritmo.

O mesmo nas três linguagens

O método é idêntico; a sintaxe muda. Aqui o mesmo problema da iteração 3:

C# — xUnit + FluentAssertions
// Red
[Fact]
public void Add_TwoNumbers_ReturnsSum() {
    Calculator.Add("2,3").Should().Be(5);
}

// Green
public static class Calculator {
    public static int Add(string s) =>
        string.IsNullOrEmpty(s)
            ? 0
            : s.Split(',').Sum(int.Parse);
}

xUnit + FluentAssertions é o padrão moderno em .NET. [Fact] para teste único; [Theory] com [InlineData] para parametrizados.

Python — pytest
# Red
def test_add_two_numbers_returns_sum():
    assert add("2,3") == 5

# Green
def add(s: str) -> int:
    if not s:
        return 0
    return sum(int(x) for x in s.split(","))

pytest tem o setup mais simples possível: função começando com test_ + assert. Sem ceremony. @pytest.mark.parametrize para casos múltiplos.

Go — testing (stdlib) table-driven
// Red + pattern table-driven
func TestAdd(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  int
    }{
        {"empty", "", 0},
        {"one number", "5", 5},
        {"two numbers", "2,3", 5},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.input); got != tt.want {
                t.Errorf("Add(%q) = %d; want %d", tt.input, got, tt.want)
            }
        })
    }
}

// Green
func Add(s string) int {
    if s == "" {
        return 0
    }
    sum := 0
    for _, p := range strings.Split(s, ",") {
        n, _ := strconv.Atoi(p)
        sum += n
    }
    return sum
}

Go favorece "table-driven tests" com subtests. É o idiom canônico — uma função de teste, casos como dados, loop. Funciona maravilhosamente com TDD: adicione caso na tabela, vermelho, implemente, verde.

Os erros típicos

Quem está aprendendo TDD costuma cair em alguns padrões previsíveis. Vale conhecer cada um pelo nome para reconhecer em si mesmo.

Pular o "red"

Você escreve teste e implementação juntos, ou implementação primeiro e teste depois "para ter cobertura". Resultado: o ganho de design some. Sintoma: testes que sempre passam de primeira porque foram escritos conhecendo a implementação. Eles não verificam o comportamento desejado; verificam o comportamento atual. Bug introduzido sem testar não vai ser pego.

Pular o "refactor"

Já mencionado: ciclo vira "red, green, próximo teste, red, green, próximo teste". Código vira spaghetti. Ganho de design perdido. Sintoma: ao olhar pro código depois de 10 ciclos, ele está duplicado, com nomes ruins, e funções quilométricas.

Passos grandes demais

Você implementa metade da feature em "uma iteração". Quando algo dá errado, não sabe qual mudança quebrou. Se o ciclo está demorando mais de 5-10 minutos entre verde e verde, está grande. Reduza.

Testes acoplados a implementação

Mocks demais, verificação de método-chamada-com-tal-parâmetro-na-tal-ordem. Resultado: refactor quebra os testes mesmo sem mudar comportamento. O teste virou retrato da implementação, não verificação de comportamento. Veja Conceito 03 (test doubles) para o tratamento.

Não testar o que importa

Testes que verificam apenas o caminho feliz. Casos de borda, condições de erro, comportamento sob input inválido — todos sem cobertura. Se o código quebrar em produção em situação não-testada, a culpa é sua, não do "input estranho". Pense adversarialmente — que input vai causar problema?

Quando TDD não cabe

TDD não é universal. Alguns contextos onde funciona mal:

A heurística é: TDD onde fortalece, não TDD onde amarra. Para a maior parte do código de domínio, fortalece. Para o resto, julgue.

Como praticar

  1. Faça o String Calculator nas três linguagens. Comece pelo zero a cada uma. Não copie; passe pelo ciclo. Note onde a sintaxe da linguagem facilita ou atrapalha.
  2. Repita FizzBuzz três vezes em uma semana. Saber a resposta não é problema — o ponto é internalizar o ritmo. Cronometre. A terceira vez deveria ser mais rápida que a primeira.
  3. Faça par com alguém em ping-pong. Ping-pong TDD: A escreve teste vermelho, B faz verde + escreve próximo teste vermelho, A faz verde + próximo teste vermelho. O ritmo é educativo e força commits atômicos.

Referências para aprofundar

  1. livro Test-Driven Development: By Example — Kent Beck (2002). A fonte. Curto, prático. Os capítulos sobre Money e xUnit valem ouro.
  2. livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce (2009). A bíblia do outside-in. Mostra TDD em projeto real, completo, com testes de aceitação dirigindo design.
  3. livro Modern Software Engineering — Dave Farley (2021). Capítulos sobre TDD como prática de engenharia (não cerimônia). Farley é co-autor de Continuous Delivery.
  4. livro The Art of Unit Testing (3rd ed.) — Roy Osherove (2024). Atualizado para .NET moderno. Capítulos 1-3 cobrem TDD na prática para iniciantes; 11+ aprofunda.
  5. artigo Test Driven Development: That's not what we meant — Steve Freeman. Resposta ao "Is TDD Dead?" — Freeman e Pryce defendem outside-in com clareza incomum.
  6. artigo The Three Laws of TDD — Robert C. Martin. cleancoder.com — formulação sintética: não escreva produção sem teste; não escreva mais teste do que falha; não escreva mais produção do que passa o teste.
  7. artigo String Calculator Kata — Roy Osherove. osherove.com/tdd-kata-1 — descrição original, com regras crescentes. O kata mais didático do gênero.
  8. artigo Mockists Are Dead — Uncle Bob (2014). blog.cleancoder.com — provocação contra mocks excessivos. Discussão saudável do trade-off Detroit vs London.
  9. docs Code Kata catalog. codingdojo.org/kata/ — catálogo aberto, comunitário. Centenas de katas categorizados por dificuldade e tema.
  10. docs Cyber-Dojo. cyber-dojo.org — ambiente online para katas em par/grupo. Várias linguagens, vários problemas.
  11. vídeo Is TDD Dead? — Beck, Fowler, DHH (2014). YouTube. Série de conversas. DHH provoca, Beck e Fowler defendem com nuance. Imperdível para entender limites.
  12. vídeo Solving Sudoku with TDD — Ron Jeffries vs Peter Norvig. YouTube/blog. Caso famoso de TDD falhando em problema mal-cabido. Lição sobre quando TDD não basta.