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:
- Como o método se chama, e em que objeto/módulo vive.
- Quais parâmetros recebe, em que ordem e tipos.
- O que retorna, em que tipo.
- Que efeito colateral causa (se for o caso).
- Que valor exato deve produzir para os inputs específicos.
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:
- Duplicação: o código que acabou de ser escrito provavelmente repete algo. Extraia. Funções, variáveis, estruturas. Beck diz que TDD é "remoção de duplicação" — é a parte que torna o código limpo emergir.
- Nomes: o que pareceu "bom o suficiente" enquanto você focava em fazer passar pode ser melhorado. Renomeie variáveis, métodos, classes para o que realmente significam.
- Estrutura: divida funções grandes em pequenas, mova responsabilidades para os lugares certos, extraia classes quando múltiplas razões para mudar aparecerem.
- Testes também: refatorar inclui melhorar os testes em si. Eliminar duplicação no setup, melhorar nomes, extrair builders.
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".
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:
- Implementação óbvia: você sabe exatamente o que escrever. Vai e escreve. Próximo teste.
- 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.
- 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:
- Inside-out tende a produzir testes menos frágeis: como focam em estado, sobrevivem a refactors estruturais. Mas pode produzir código que "escala mal" se o desenho de fronteira não foi pensado.
- Outside-in força pensar a fronteira primeiro: você decide a API que o cliente verá antes de pensar em implementação. Mas produz testes mais frágeis (mocks acoplados a chamadas) e exige manutenção maior.
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:
- FizzBuzz: o mais simples. Imprima 1 a 100; múltiplos de 3, "Fizz"; de 5, "Buzz"; de ambos, "FizzBuzz". Trivial, mas o ritmo de TDD aplicado nele é a base de tudo.
- String Calculator de Roy Osherove: receba uma string com números separados, retorne soma. Adicionando regras incrementais (separadores customizados, ignorar negativos, etc.). Excelente para triangulação.
- Bowling Game de Bob Martin: calcule pontuação de jogo de boliche com strikes e spares. Famoso porque a solução final é contraintuitivamente simples — TDD a faz emergir.
- Roman Numerals: converter inteiro em romano. Forçar triangulação até a generalização final. Ótimo para entender o "fake it".
- Bank Account Kata: depósito, saque, extrato. Mais próximo de domínio real, com regras de negócio (saldo não negativo).
- Mars Rover: rover recebe comandos N/S/L/W/F. Útil para praticar outside-in com objetos colaborando.
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:
// 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.
# 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.
// 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:
- UI exploratória: você está descobrindo qual deveria ser a interface. Testes prematuros amarram exploração. Faça spike, descarte, refaça com TDD quando souber o desenho.
- Integração com API desconhecida: você precisa entender os retornos reais antes de testar. Faça curl/postman primeiro; depois TDD em cima.
- Códigos de uso único: scripts de migração, análises ad-hoc. TDD é overhead aqui.
- Glue code trivial: configurações, wiring de DI. Testar que o framework funciona é redundante.
- Performance crítica: às vezes você precisa medir primeiro, descobrir o gargalo, otimizar. Otimização guiada por testes de comportamento sem benchmark é cega.
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
- 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.
- 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.
- 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
- livro Test-Driven Development: By Example — Kent Beck (2002).
- livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce (2009).
- livro Modern Software Engineering — Dave Farley (2021).
- livro The Art of Unit Testing (3rd ed.) — Roy Osherove (2024).
- artigo Test Driven Development: That's not what we meant — Steve Freeman.
- artigo The Three Laws of TDD — Robert C. Martin.
- artigo String Calculator Kata — Roy Osherove.
- artigo Mockists Are Dead — Uncle Bob (2014).
- docs Code Kata catalog.
- docs Cyber-Dojo.
- vídeo Is TDD Dead? — Beck, Fowler, DHH (2014).
- vídeo Solving Sudoku with TDD — Ron Jeffries vs Peter Norvig.