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.

ImportanteA ideia central que você deve carregar

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.

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

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.

import 'package:test/test.dart';

void main() {
  test('carrinho vazio tem total zero', () {
    final carrinho = Carrinho();
    expect(carrinho.total, 0.0);
  });
}

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.

class Carrinho {
  double get total => 0.0;
}

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.

test('carrinho soma o preço de um item', () {
  final carrinho = Carrinho();
  carrinho.adicionar(Item('Livro', 50.0, 1));
  expect(carrinho.total, 50.0);
});

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.

double get total =>
    _itens.fold(0.0, (soma, item) => soma + item.preco * item.quantidade);

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.

double get totalComFrete {
  const limiteFreteGratis = 100.0;
  const valorFrete = 15.0;
  return total >= limiteFreteGratis ? total : total + valorFrete;
}

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.

test('cupom de 10% reduz o subtotal antes do frete', () {
  final carrinho = Carrinho();
  carrinho.adicionar(Item('Fone', 200.0, 1));
  carrinho.aplicarCupom(0.10);
  expect(carrinho.totalComFrete, 180.0);
});

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.

DicaTestes como a documentação que nunca envelhece mal

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.