Introdução
A primeira vez que eu ouvi falar de TDD foi em 2004. Na época, trabalhava com Delphi ainda. Um colega da minha equipe trouxe o livro do Kent Beck, traduzido em português (acho que era esse: Programação extrema aplicada), sobre XP e estávamos querendo revolucionar como as coisas eram feitas.
Na época, principalmente em Delphi, pouco falava-se de design de software. Orientação a objetos, SOLID era uma coisa muito falada e pouco entendida. Foi um dos meus primeiros projetos que tentei fazer uma separação real de UI da lógica de negócio, criando objetos de domínio.
Foi um tempo muito divertido, e motivado pelas idéias do Beck fiz minha primeira implementação de integração contínua: um arquivo .bat, rodando um script no “want” (uma versão de ant para Windows, bem aderente ao Delphi. Não acho um link sobre isso mais!) no agendador da máquina (Windows) e no cron da máquina (Linux), pois o projeto era multi plataforma.
Outra coisa que aplicamos foram os testes associados ao build usando o DUnit.
A primeira experiência com testes
Neste projeto, conseguíamos testar os objetos de domínio, pois a UI estava totalmente desacoplada dele, porém, por um erro de design, a persistência não era desacoplada da lógica de negócios, o que automaticamente forçava os testes a dependerem de banco.
Usávamos então um setup que criava registros no banco e forçávamos os testes a executarem em ordem para que tudo passasse. Os testes foram muito úteis porque o projeto também era multi-banco e dessa forma, conseguíamos testar se nossa persistência funcionava em todos eles.
Não demorou muito para os testes começarem a ser abandonados, pois era trabalhoso demais criar os testes, eles eram muito lentos pra executar e praticamente todos orientados ao banco de dados (como era o pensamento que imperava na época).
Tive uma experiência negativa do processo.
As experiências posteriores
Dali pra frente (já em .NET), sempre adotei os testes como uma prática opcional. Quando tinha alguma regra de negócio mais complexa ou componentes de infraestrutura sempre criava os testes após o desenvolvimento. Como já tinha alguma experiência, tinha facilidade em visualizar o design up front.
Óbvio que depois os testes sofriam um viés muito forte daquilo que já estava desenvolvido. Ajudava bastante, mas sem saber estava praticando “design done by tests“.
Fazendo as pazes com TDD
Recentemente, resolvi prestigiar o trabalho do Maurício Aniche. Ele escreveu o livro Test-Driven Development: Teste e Design no Mundo Real. Confesso que eu comprei o livro porque eu conhecia o Aniche pessoalmente e queria prestigiar o trabalho de um brasileiro. Só não esperava que eu iria me deparar com um livro extremamente bem escrito e com um ponto de vista para mim novo sobre TDD: Os testes influenciam o design.
O livro me ajudou a entender que quando escrevemos os testes antes do código, o teste vai nos forçando a criar um bom design, vai forçando o SOLID a ser aplicado no nosso código.
Isso acontece porque se você está com dificuldade de pensar ou de criar o teste, é porque provavemente suas reponsabilidades não estão bem separadas, ou você não está invertendo as dependências corretamente.
O teste é quem vai te contando isso na prática. O grande problema é aprendermos a dar ouvidos ao que a prática nos diz.
Como funciona isso na prática?
Se eu de fato tivesse aprendido a aplicar o TDD corretamente na minha primeira experiência, teria percebido que se eu estou dependendo do banco de dados pra fazer um teste de regra de negócios, algo está errado. Esse é o tipo do sintoma que tendemos a ignorar por causa do apego que acabamos desenvolvendo ao nosso design.
O mesmo acontece com qualquer dependência externa. Um arquivo, um web service, qualquer coisa desse tipo pode ter suas dependências invertidas para ser corretamente testada. No código do MSBuildCodeMetrics, tive uma situação similar para testar as tasks sem depender do MSBuild.
Testes de integração não são ruins, mas se você não tem unidades, não consegue fazer testes de unidade. Esse é um dos grandes benefícios.
Se eu também tivesse lido um pouco mais e procurado na comunidade informações sobre como aplicar melhor o princípio, descobriria coisas importantes como a atomicidade dos testes. Os testes não podem depender de outros testes, o que tornou a manutenção dos mesmos muito difícil.
Documentação do Design
O TDD também nos ajuda a documentar a nossa “intenção” quando criamos um design. Esse é outro grande benefício. Um exemplo que eu gosto de dar é:
public Cliente localizarClienteComMaiorVolumeDeVendas(IList<Pedido> pedidos){ }
No método acima, qual o comportamento você espera quando nenhum cliente é encontrado (Ex.: lista de pedidos vazia)? Você espera que o método retorne null ou dê um throw numa Exception? Os testes conteriam essa resposta. E melhor ainda, se alguém mudar este comportamento no método, o teste quebrará, o que garantirá a evolução deste design no decorrer do tempo e evitar bugs indesejados.
Produtividade
Produtividade é um critério que sempre gera muita discussão na comunidade de desenvolvimento. Ela aparece em diversas discussões: arrastar componentes invés de criar código manualmente, utilizar ferramentas de migração de código e não podia ser diferente, nos testes. A grande pergunta é se o tempo que os desenvolvedores perdem escrevendo teste paga os benefícios ou não. É bastante intangível isto, apesar de existiram muitas pesquisas discutindo os benefícios, essa discussão aparece.
Após ler o Clean Code e o The Clean Coder, do Uncle Bob, eu me convenci facilmente que qualquer desenvolvedor profissional deveria praticar TDD, assim como me convenci que a grande dificuldade de falar sobre produtividade em software é que confundimos o custo do primeiro release com o custo do software.
Deixa eu tentar explicar melhor: sempre pensamos em produtividade no tempo que o produto demora pra ficar “pronto”, ir para o ar, subir e começar a trazer dinheiro ou resultado. Mas quanto custa evoluir este produto depois?
Sempre que o código apodrece (termo usado pelo Uncle Bob), o custo para mexer em qualquer coisa é exponencial. Começam a surgir bugs nos lugares mais improváveis e cada vez mais tendemos a querer jogar tudo fora e reescrever o software todo. Qual o custo disso?
A forma mais simples de entender essa questão da produtividade vem do gráfico abaixo. É um gráfico de produtividade em relação ao tempo:
A idéia é que no começo, quando você não carrega nenhum código podre, sua produtividade é 100 e com o decorrer do tempo ela tende a nada. Minha experiência empírica me diz que isso é verdade.
As práticas de XP (TDD, Integração Contínua, Refatoração) são aliados importantes para ajudar o gráfico acima se tornar uma reta e não ter a cara de um logarítmo. Na partida, nos sentimos mais lentos, mas com o decorrer do tempo, a produtividade se torna constante.
Era isso!
Eric, muito boa a sua abordagem sobre o tema. Confesso que ainda não aplico TDD no meu dia a dia ainda por falta de conhecimento mas é perceptível que em mais de 70% das vezes é mais fácil reescrever um código do que melhorá-lo. Tenho lido bastante sobre TDD e essas dicas do seu texto irão me ajudar bastante, com certeza.
Muito obrigado!
Bacana Sócrates,
Se você está em fase de aprendizado, ainda está na dúvida, leia o livro do Aniche. Vai te cortar um bom caminho.
Abraço,
Eric
Obrigado pela dica!
Ótimo texto, Eric.
Ótimo texto, e ótima dica. Já vi videos com o Aniche, e ele é excelente. Me interessei pelo livro, irá me ajudar muito no meu TCC da pós graduação, que será sobre TDD.