Um teste escrito depois do código tende a verificar o que o código faz, não o que ele deveria fazer. O desenvolvimento guiado por testes (TDD) resolve isso invertendo a ordem: você escreve o teste, vê ele falhar, e então escreve o menor código possível que o faz passar. Como o teste existe antes do código que ele verifica, ele não pode confirmar silenciosamente o que quer que o código retorne. Kent Beck formalizou esse ciclo como red, green, refactor em seu livro de 2002 Test-Driven Development: By Example. O nome pegou.
O que o ciclo red-green-refactor realmente faz
Cada volta do ciclo tem três passos, e pular um deles o transforma em outra coisa.
Red. Escreva um teste para um comportamento que ainda não existe e execute-o. Ele tem que falhar. Se passar de primeira, ou o comportamento já existe, ou o teste não está verificando nada real. Uma execução vermelha é a prova de que o teste consegue pegar o bug que deveria pegar.
Green. Escreva a menor quantidade de código de produção que faz o teste passar. Não o melhor código. O código mínimo. Retornar um valor fixo no código é um passo green legítimo, desde que você saiba que foi isso que fez e não pare por aí.
Refactor. Com o teste passando, limpe o que você acabou de escrever: remova duplicação, ou extraia uma função se fizer sentido. O teste precisa continuar verde o tempo todo, porque é assim que você sabe que o comportamento não mudou, só a forma do código.
Depois escolha o próximo comportamento e recomece. Uma iteração leva minutos: um branch if, um caso de borda, nunca uma funcionalidade inteira.
Um exemplo de TDD em Python: o kata FizzBuzz
O FizzBuzz é pequeno o suficiente para você ver o ritmo em vez do problema. Dado um número, retorna "Fizz" se ele for divisível por 3, "Buzz" se for divisível por 5, "FizzBuzz" se for divisível por ambos, e o próprio número caso contrário.
Comece pelo caso que não é nenhum desses:
| |
| |
Vermelho, e você não precisou forçar a falha de propósito: fizzbuzz ainda não existe, então o pytest nem consegue importar o módulo de teste. Ainda assim é um vermelho válido: a correção é a mesma nos dois casos. A coisa mais simples que faz passar:
| |
| |
Verde. Próximo comportamento: múltiplos de 3.
| |
Isso falha (assert '3' == 'Fizz'), e a correção mínima é um único branch:
| |
Múltiplos de 5 seguem o mesmo padrão, então adicione o teste e o elif correspondente. Depois escreva o caso que expõe o descuido deixado pelos dois primeiros passos: um número divisível por ambos.
| |
| |
15 % 3 == 0 é verificado primeiro, então a função retorna "Fizz" antes mesmo de olhar para 15 % 5. Aqui a ordem importa. A correção é um branch para 15 que roda antes dos outros dois:
| |
| |
Verde de novo, e agora o arquivo de testes tem algo que vale a pena refatorar: quatro funções quase idênticas. Juntá-las em um único teste parametrizado conta como refactor tanto quanto qualquer coisa que você faria em fizzbuzz.py, desde que o verde se mantenha o tempo todo.
| |
O bug da ordem só apareceu porque o teste para 15 já existia antes da correção. Escreva o código primeiro e pare em “3 dá Fizz, 5 dá Buzz, parece pronto”, e fizzbuzz(15) retorna "Fizz" sem nada na suíte para avisar.
Por que código escrito test-first acaba fracamente acoplado
O TDD muda o design antes mesmo de qualquer teste rodar, porque você precisa escrever o código de um jeito que o teste consiga chamá-lo. Pegue uma classe que constrói seu próprio repositório de banco de dados internamente, o mesmo padrão com que O Que É Dependency Injection começa. Você não consegue passar um fake para ela em um teste, porque nada na assinatura dela permite substituir essa dependência. Escrever o teste primeiro empurra essa dependência para um parâmetro ou argumento de construtor antes mesmo de o código de produção existir para resistir a isso. Você não adiciona o baixo acoplamento depois, como um bônus. Sem ele, o teste que você está prestes a escrever não roda.
A mesma pressão empurra as funções a fazerem uma coisa só. Se um teste precisa montar três pedaços de estado sem relação entre si para verificar um comportamento, a função sob teste faz três coisas sem relação entre si. Divida-a, e o teste fica mais curto.
Quando não usar TDD
O ciclo precisa de algumas condições: você sabe o comportamento que quer antes de escrevê-lo, os testes rodam de novo em segundos, e o código tem um ponto onde um test double pode entrar. Perca qualquer uma delas e o ciclo para de compensar. Mesmo quando todas se mantêm, ele tem um ponto cego.
Você ainda não sabe a forma da coisa. Se você está explorando uma API desconhecida ou fazendo um spike para ver se uma abordagem é viável, escreva o código descartável primeiro. Aprenda qual é a abstração certa, depois escreva os testes contra a versão que pretende manter. Testes para código que você está prestes a apagar são esforço gasto duas vezes.
O ciclo de feedback é lento. TDD significa rodar os testes de novo a cada pequena mudança. Uma suíte que bate em um banco de dados real ou na rede a cada execução transforma cada uma dessas repetições em espera. Teste Unitário vs Teste de Integração vs End-to-End explica por que a velocidade no nível unitário vem de trocar essas dependências. Se os únicos testes que você consegue escrever para um trecho de código são testes de integração lentos, testar esse código com TDD significa esperar, e as pessoas param de fazer coisas que as fazem esperar.
Ainda não existe uma costura. Código legado, com construtores que acessam infraestrutura real e funções que se chamam diretamente umas às outras, não dá para fazer TDD a partir de onde está. Não tem onde colocar um double. O Que É Spaghetti Code mostra o caminho de entrada: um characterization test que fixa primeiro o comportamento existente, escrito contra o código como ele é. Depois que essa rede existe, red-green-refactor funciona para a próxima mudança.
Mock em excesso deixa passar design ruim de qualquer jeito. Um teste que faz mock de cada colaborador pode ficar verde numa função que por dentro é uma bagunça, desde que as chamadas dela batam com o que os mocks esperam. O TDD mantém sua atenção na única unidade na sua frente. De vez em quando você ainda precisa dar um passo atrás e perguntar se as unidades têm a forma certa.
Como começar com TDD: corrija um bug com TDD
Escolha um bug, não uma funcionalidade, para sua primeira tentativa. Escreva um teste que o reproduza: chame a função com o input que o quebra, verifique o output que você realmente quer obter, e veja ele falhar pelo motivo certo antes de tocar na correção. A correção vira o passo green, e o teste fica na suíte para que o bug não possa voltar sem ninguém perceber. Faça um commit a cada volta. Um rastro de commits pequenos e sempre verdes é o que deixa git bisect rápido na próxima vez que algo quebrar e ninguém lembrar onde.