stateDiagram-v2
[*] --> Vermelho
Vermelho --> Verde: escreve o mínimo para passar
Verde --> Refatorar: teste passou
Refatorar --> Vermelho: novo comportamento a especificar
Refatorar --> [*]: funcionalidade completa
note right of Vermelho
escreve um teste
que falha
end note
note right of Verde
código mínimo,
sem elegância ainda
end note
note right of Refatorar
melhora a forma,
preserva o comportamento
end note
Módulo 13 — Desenvolvimento Orientado a Testes (TDD)
Onde estamos. No módulo anterior você aprendeu a escrever testes que verificam se o código faz o que deveria. Agora vamos inverter a ordem e questionar uma intuição óbvia: será que o teste vem depois do código? O Desenvolvimento Orientado a Testes propõe o contrário, e essa inversão, longe de ser um truque, muda a forma como você projeta software. Quero que você chegue à aula entendendo por que escrever o teste primeiro é, antes de tudo, uma decisão de projeto.
Deixe-me começar com uma provocação honesta. Você provavelmente já escreveu testes, e talvez tenha achado a experiência um pouco chata: o código já estava pronto e o teste parecia uma formalidade que confirmava o óbvio. Se foi assim, você experimentou o teste no seu papel mais fraco. Hoje discutiremos uma prática em que o teste deixa de ser verificação tardia e passa a ser o motor que conduz o desenho do código. A ideia soa quase paradoxal — como testar algo que ainda não existe? —, mas é justamente essa aparente impossibilidade que carrega a lição.
A origem e a ideia central
O Desenvolvimento Orientado a Testes, ou TDD, da sigla em inglês para Test-Driven Development, foi articulado e popularizado por Kent Beck no contexto do movimento ágil e da Programação Extrema, no início dos anos 2000. Beck não afirmava ter inventado a ideia; ele próprio contava tê-la redescoberto ao ler um livro antigo que sugeria escrever a saída esperada de um programa antes de escrevê-lo. O que Beck fez foi transformar essa intuição solta em uma disciplina, com regras claras e ritmo repetível, e demonstrar que ela sustentava a construção de sistemas reais.
A ideia central é enganosamente simples: você não escreve uma linha de código de produção sem antes ter um teste que falhe pedindo por ela. O teste vem primeiro, o código depois, sempre nessa ordem. Isso reposiciona o papel do teste, que deixa de ser uma rede de segurança estendida depois de já ter pulado e passa a ser a especificação executável do que você está prestes a construir. Antes de existir código, existe uma afirmação precisa sobre o comportamento desejado, numa linguagem que a máquina pode verificar.
Em TDD, o teste não verifica o código: o teste conduz o código. Você especifica o comportamento desejado como um teste que falha, depois escreve o mínimo necessário para satisfazê-lo e, por fim, melhora a estrutura sem alterar o comportamento. Não é uma técnica de teste que aconteceu de vir antes; é uma técnica de projeto que usa o teste como ferramenta de pensamento.
O ciclo vermelho-verde-refatoração
O coração do TDD é um ciclo curto e repetitivo, apelidado pelas cores que aparecem na interface dos frameworks de teste: vermelho quando um teste falha, verde quando ele passa. O ciclo tem três fases, e a ordem entre elas é inegociável.
A primeira fase é o vermelho. Você escreve um teste para um comportamento que ainda não existe. Como o código correspondente não foi escrito, o teste necessariamente falha — e essa falha é boa, esperada, é o ponto de partida. Aqui está uma sutileza que muitos ignoram: você deve ver o teste falhar. Um teste que passa antes de você escrever o código de produção é um teste quebrado, que testa a coisa errada ou não testa nada. Ver o vermelho confirma que o teste exercita o comportamento pretendido e tem poder de detectar sua ausência.
A segunda fase é o verde. Agora você escreve código de produção com um único objetivo: fazer o teste passar. Não o melhor código, não o mais elegante, apenas o mínimo suficiente para transformar o vermelho em verde. Beck insiste nesse minimalismo quase incômodo — às vezes defende que se retorne um valor fixo, uma constante literal, se isso bastar para passar o teste atual. A intenção não é produzir código bobo, e sim resistir à tentação de projetar demais antecipadamente. O código só cresce quando um novo teste o obriga a crescer.
A terceira fase é a refatoração. Com o teste passando, você tem uma rede de segurança que permite melhorar a estrutura interna sem medo. Refatorar significa alterar a forma sem alterar o comportamento: eliminar duplicação, extrair um método, renomear uma variável para revelar sua intenção, reorganizar uma condicional. Cada pequena melhoria é seguida de nova execução dos testes; se continuam verdes, o comportamento foi preservado. É nesta fase que o código deixa de “apenas funcionar” e passa a ser código que você teria orgulho de mostrar.
Note que o ciclo é deliberadamente pequeno. Cada volta cobre um fragmento minúsculo de comportamento. Essa granularidade fina dá ao TDD sua sensação característica: você quase nunca fica distante de um estado verde e conhecido. Se algo quebra, sabe exatamente o que quebrou, porque mudou pouca coisa desde a última vez que tudo estava bem.
As regras práticas do TDD
Beck resumiu a disciplina em duas regras que guiam todo o resto. A primeira: escreva código de produção apenas para fazer passar um teste que está falhando. A segunda: escreva apenas o teste suficiente para falhar — e não compilar já conta como falhar. Dessas duas regras deriva o ritmo que você viu. Elas são restritivas de propósito: a restrição é o que impede tanto o excesso de teste quanto o excesso de código especulativo. Alguns hábitos complementam essas regras, e vale registrá-los na tabela abaixo.
| Hábito | Por que importa |
|---|---|
| Um teste falhando de cada vez | Se dois testes falham juntos, você perde a clareza sobre qual comportamento está construindo. |
| Passos pequenos, sempre | Quanto menor o passo, mais fácil localizar a causa quando algo dá errado. |
| Ver o vermelho antes do verde | Confirma que o teste realmente exercita o comportamento e não passa por acidente. |
| Refatorar só com tudo verde | Mexer na estrutura sem a rede de testes é editar no escuro. |
| Nunca refatorar e adicionar comportamento juntos | São duas atividades distintas; misturá-las esconde a origem dos erros. |
Essa última linha merece uma pausa. Refatorar e implementar novo comportamento são operações de naturezas diferentes: uma preserva o comportamento, a outra o modifica. Se você faz as duas juntas e algo quebra, não sabe se a culpa foi da nova funcionalidade ou da reorganização. Mantê-las separadas — cada uma no seu momento do ciclo — é o que torna o processo confiável.
Um exemplo completo, passo a passo, em Dart
Teoria sem prática, aqui, não convence. Vamos construir juntos uma funcionalidade real seguindo o ciclo à risca: um cálculo de carrinho de compras que soma itens, aplica frete e concede desconto conforme uma regra de negócio. Evoluímos por iterações, sempre começando pelo teste.
Iteração 1 — o carrinho vazio (vermelho). Começo pelo caso mais simples: um teste afirmando que um carrinho sem itens tem total zero. O código do carrinho ainda não existe, então isto nem compila — e, pelas regras, não compilar já é o vermelho.
Iteração 1 — o mínimo para o verde. Agora escrevo o código mínimo que faz o teste passar. Resisto à tentação de já pensar em itens, frete ou desconto. O teste só pede um total zero, então entrego exatamente isso.
O teste passa. Pode parecer ridículo devolver uma constante, mas é o que Beck prescreve: não invente comportamento que nenhum teste pediu. Não há o que refatorar ainda, então sigo.
Iteração 2 — somar um item (vermelho). Adiciono um teste que exige que o carrinho some o preço de um item. Ele falha, porque total ainda devolve zero fixo.
Iteração 2 — o mínimo para o verde. Introduzo a classe Item e faço o carrinho realmente somar. O total deixa de ser constante e passa a percorrer os itens.
class Item {
final String nome;
final double preco;
final int quantidade;
Item(this.nome, this.preco, this.quantidade);
}
class Carrinho {
final List<Item> _itens = [];
void adicionar(Item item) => _itens.add(item);
double get total {
var soma = 0.0;
for (final item in _itens) {
soma += item.preco * item.quantidade;
}
return soma;
}
}Repare que já contemplei quantidade na multiplicação — uma pequena antecipação aceitável, porque o modelo do item naturalmente a inclui. Rodo os testes: os dois passam, verde. E aqui já cabe uma refatoração honesta, pois o laço explícito pode virar uma expressão mais declarativa.
Rodo os testes de novo: continuam verdes. O comportamento é o mesmo, a forma melhorou. Este é o ciclo funcionando.
Iteração 3 — frete grátis acima de um limite (vermelho). Agora entra uma regra de negócio de verdade: compras abaixo de cem reais pagam quinze de frete; a partir de cem, o frete é grátis. Escrevo dois testes que capturam os dois lados da regra.
test('compra abaixo de 100 paga frete de 15', () {
final carrinho = Carrinho();
carrinho.adicionar(Item('Caneta', 40.0, 1));
expect(carrinho.totalComFrete, 55.0);
});
test('compra a partir de 100 tem frete gratis', () {
final carrinho = Carrinho();
carrinho.adicionar(Item('Mochila', 120.0, 1));
expect(carrinho.totalComFrete, 120.0);
});Iteração 3 — o mínimo para o verde. Implemento a regra do frete de forma direta.
Os quatro testes passam. Na refatoração, extraio os números mágicos para constantes nomeadas na classe, tornando a regra explícita e fácil de ajustar, sem tocar no comportamento.
Iteração 4 — desconto de cupom (vermelho). Por fim, um cupom percentual sobre o subtotal, aplicado antes do frete. Escrevo o teste primeiro, como sempre.
Iteração 4 — o mínimo para o verde, e a refatoração final. Faço o cupom incidir sobre o subtotal e ajusto o cálculo. Depois de verde, reorganizo a classe para que cada responsabilidade tenha um nome claro.
class Carrinho {
static const double _limiteFreteGratis = 100.0;
static const double _valorFrete = 15.0;
final List<Item> _itens = [];
double _desconto = 0.0;
void adicionar(Item item) => _itens.add(item);
void aplicarCupom(double percentual) => _desconto = percentual;
double get subtotal =>
_itens.fold(0.0, (soma, item) => soma + item.preco * item.quantidade);
double get subtotalComDesconto => subtotal * (1 - _desconto);
double get totalComFrete => subtotalComDesconto >= _limiteFreteGratis
? subtotalComDesconto
: subtotalComDesconto + _valorFrete;
}Percorra mentalmente o caminho trilhado. Nunca escrevemos uma linha que algum teste não tenha exigido, e a estrutura da classe emergiu das necessidades reais, não de um projeto especulativo feito de antemão. Este é o sentido de dizer que o TDD guia o design: a interface pública — adicionar, aplicarCupom, subtotal, totalComFrete — foi desenhada do ponto de vista de quem usa, porque escrevemos o uso antes da implementação.
Os benefícios que justificam a disciplina
O primeiro benefício, e o mais subestimado, é que o TDD produz design guiado pelo uso. Ao escrever o teste primeiro, você é forçado a decidir como a funcionalidade será chamada antes de decidir como ela funciona por dentro. Isso conduz a interfaces mais limpas, porque você experimenta o código na posição de cliente e sente o desconforto de uma API ruim antes de tê-la construído.
O segundo é o feedback rápido. O ciclo curto mantém você perto de um estado verde conhecido. Quando algo quebra, o intervalo desde a última certeza é tão pequeno que localizar a causa é quase trivial. Compare com a depuração de um sistema desenvolvido por horas sem testar: o espaço de busca do defeito é enorme.
O terceiro benefício é que os testes se tornam documentação viva. Um teste bem escrito é uma afirmação executável sobre o comportamento do sistema — e, ao contrário de um comentário ou de um documento em separado, não pode mentir por muito tempo: se o comportamento muda e o teste não acompanha, ele falha e denuncia a divergência. Quem chega ao código depois de você lê os testes para entender o que o sistema promete fazer.
Documentação escrita à parte tende a apodrecer: o código muda, o texto não, e em pouco tempo o documento passa a mentir. Um teste é diferente porque é executado. Se ele diverge do comportamento real, quebra imediatamente. Por isso Robert Martin costuma tratar a suíte de testes como a especificação mais honesta que um sistema possui.
O quarto benefício, talvez o mais transformador, é a confiança para refatorar. Melhorar a estrutura de um código sem testes é uma aposta: você espera não ter quebrado nada, mas não sabe. Com uma rede densa de testes, refatorar vira operação segura: você reorganiza, roda os testes, e a barra verde confirma que o comportamento foi preservado. Essa segurança impede o software de deteriorar — aquele fenômeno de corrosão estrutural que discutimos no início do curso. Sem testes, o medo de mexer congela o código; com testes, o código permanece maleável.
TDD comparado a escrever testes depois
É natural perguntar se não daria no mesmo escrever o código e depois cobri-lo com testes, já que ao final de ambos os caminhos existem código e testes. A resposta é que os dois processos produzem resultados sistematicamente diferentes, e a tabela a seguir organiza o contraste.
| Aspecto | TDD (teste antes) | Testes depois |
|---|---|---|
| Papel do teste | Guia o desenho da interface | Verifica um desenho já fixado |
| Influência no design | Alta; a API nasce do uso | Baixa; a API já está dada |
| Cobertura | Naturalmente alta, cada linha nasceu de um teste | Frequentemente irregular, com lacunas |
| Momento de detecção do erro | No instante em que é introduzido | Possivelmente muito depois |
| Risco de testar por conveniência | Baixo; o teste precede o código | Alto; tende-se a testar o que é fácil |
O ponto mais delicado é o último. Quando você escreve o teste depois, com o código já pronto diante dos olhos, há uma tendência quase inevitável de escrever testes que confirmam o que o código faz, em vez de verificar o que ele deveria fazer. O teste se molda ao código, e não o contrário. Além disso, caminhos difíceis de testar tendem a ficar sem teste — exatamente os que mais precisariam. No TDD, como o teste vem primeiro, essa acomodação não tem por onde acontecer.
Não quero, porém, ser dogmático. Escrever testes depois é infinitamente melhor do que não escrever teste algum, e há situações — código legado, exploração de uma API desconhecida — em que começar pelo teste é difícil ou improdutivo. O TDD é uma disciplina poderosa, não um mandamento absoluto. O que você deve levar é a compreensão de por que a ordem importa, para decidir com consciência quando segui-la.
Dublês de teste: mocks e stubs em conceito
À medida que o código cresce, aparece uma dificuldade: como testar uma unidade que depende de outras coisas — um banco de dados, um serviço de rede, o relógio do sistema — sem arrastar todas essas dependências para dentro do teste? A resposta são os dublês de teste, objetos que substituem colaboradores reais durante o teste, assim como um dublê substitui o ator numa cena perigosa.
Dois tipos merecem sua atenção conceitual agora. Um stub fornece respostas prontas: você o configura para devolver um valor fixo quando consultado, isolando a unidade sob teste da lógica real do colaborador. Se sua função depende de uma cotação de câmbio vinda da rede, um stub devolve uma cotação combinada, e o teste fica rápido e determinístico. Um mock vai além: não apenas responde, mas também verifica como foi usado. Com um mock, você pode afirmar que um método foi chamado, com certos argumentos, um certo número de vezes. O stub responde à pergunta “o que a unidade recebe?”; o mock, à pergunta “o que a unidade faz com seus colaboradores?”.
flowchart LR
T[Teste] --> U[Unidade sob teste]
U --> S[Stub: devolve resposta pronta]
U --> M[Mock: registra e verifica chamadas]
M -. confirma interação .-> T
style T fill:#cfe2ff,stroke:#084298
style U fill:#d1e7dd,stroke:#0f5132
style S fill:#fff3cd,stroke:#664d03
style M fill:#f8d7da,stroke:#842029
Guarde a distinção pela intenção, não pela ferramenta. Um stub existe para fornecer dados e desacoplar a unidade de dependências lentas ou imprevisíveis. Um mock existe para verificar comportamento, confirmando que a unidade interagiu com seus colaboradores da forma esperada. Confundir os dois leva a testes frágeis, presos a detalhes internos que deveriam poder mudar.
Uma advertência de quem já se queimou: abusar de mocks acopla o teste à implementação. Se você verifica cada chamada interna, qualquer refatoração legítima quebra os testes, e a rede de segurança vira camisa de força. Use dublês para isolar fronteiras reais — rede, disco, tempo —, não para espionar a mecânica interna de uma unidade que deveria poder ser reorganizada livremente.
Como este módulo se conecta ao restante do curso
No módulo anterior você aprendeu a testar; aqui inverteu a ordem e descobriu que o teste, escrito primeiro, é uma ferramenta de projeto. As duas ideias se completam: sem saber escrever um bom teste, o TDD é impossível; sem o TDD, você não experimenta o teste no seu papel mais forte. Adiante, quando estudarmos qualidade e manutenção, reconhecerá na suíte de testes a condição que torna a evolução do software segura em vez de temerária — a confiança para mudar que separa um sistema vivo de um sistema congelado pelo medo.
Síntese
Quero que você retenha três ideias desta conversa. A primeira é que o TDD, como Kent Beck o formulou, não é uma técnica de teste, mas uma técnica de projeto: escrever o teste antes força você a pensar no uso antes da implementação, e disso nasce um design mais limpo. A segunda é o ciclo vermelho-verde-refatoração, com sua ordem inegociável — falhar de propósito, passar com o mínimo, melhorar com segurança — e sua granularidade fina, que mantém você sempre perto de um estado conhecido. A terceira é que a rede de testes construída assim entrega o benefício que mais importa a longo prazo: a confiança para refatorar, que é o que mantém o software maleável e o protege da deterioração. Chegue à aula com essas três ideias assentadas.
Para consolidar antes da aula: escolha uma função pequena que você já escreveu e reconstrua-a do zero pelo TDD, começando pelo teste que falha e avançando um passo de cada vez. Preste atenção no desconforto de ver o vermelho e na tentação de escrever mais código do que o teste pede. Se você conseguir descrever como a ordem inversa mudou o desenho da sua função, entendeu o essencial deste módulo.