Breve introdução a esta pergunta. Eu tenho usado agora TDD e ultimamente BDD há mais de um ano. Uso técnicas como zombaria para tornar a escrita de meus testes mais eficiente. Ultimamente, comecei um projeto pessoal para escrever um programa de gerenciamento de dinheiro para mim. Como não tinha código legado, era o projeto perfeito para começar com o TDD. Infelizmente, não senti tanto a alegria do TDD. Até estragou tanto a minha diversão que desisti do projeto.
Qual era o problema? Bem, eu usei a abordagem do tipo TDD para permitir que os testes / requisitos evoluam o design do programa. O problema era que mais da metade do tempo de desenvolvimento era para testes de escrita / refatoração. Portanto, no final, eu não queria implementar mais recursos, porque precisaria refatorar e gravar em muitos testes.
No trabalho, tenho muito código legado. Aqui, escrevo cada vez mais testes de integração e aceitação e menos testes de unidade. Isso não parece ser uma abordagem ruim, pois os bugs são detectados principalmente pelos testes de aceitação e integração.
Minha ideia era que, no final, eu pudesse escrever mais testes de integração e aceitação do que testes de unidade. Como eu disse para detectar bugs, os testes de unidade não são melhores do que os testes de integração / aceitação. Teste de unidade também é bom para o design. Como eu escrevia muitas delas, minhas aulas são sempre projetadas para serem testáveis. Além disso, a abordagem para permitir que os testes / requisitos guiem o design leva, na maioria dos casos, a um design melhor. A última vantagem dos testes de unidade é que eles são mais rápidos. Eu escrevi testes de integração suficientes para saber que eles podem ser quase tão rápidos quanto os testes de unidade.
Depois de pesquisar na web, descobri que existem idéias muito semelhantes às minhas aqui e ali . O que você pensa dessa ideia?
Editar
Respondendo às perguntas, um exemplo em que o design foi bom, mas eu precisava de uma refatoração enorme para o próximo requisito:
No início, havia alguns requisitos para executar determinados comandos. Escrevi um analisador de comando extensível - que analisou comandos de algum tipo de prompt de comando e chamou o correto no modelo. O resultado foi representado em uma classe de modelo de exibição:
Não havia nada errado aqui. Todas as classes eram independentes uma da outra e eu podia facilmente adicionar novos comandos, mostrar novos dados.
O próximo requisito era que todo comando tivesse sua própria representação de vista - algum tipo de visualização do resultado do comando. Redesenhei o programa para obter um design melhor para o novo requisito:
Isso também foi bom porque agora todos os comandos têm seu próprio modelo de visualização e, portanto, sua própria visualização.
O fato é que o analisador de comandos foi alterado para usar uma análise baseada em token dos comandos e foi retirado de sua capacidade de executar os comandos. Todo comando possui seu próprio modelo de visualização e o modelo de visualização de dados conhece apenas o modelo atual de visualização de comando que conhece os dados que devem ser mostrados.
Tudo o que eu queria saber neste momento é se o novo design não quebrou nenhum requisito existente. Não precisei alterar QUALQUER teste de aceitação. Eu tive que refatorar ou excluir quase TODOS os testes de unidade, o que foi uma enorme pilha de trabalho.
O que eu queria mostrar aqui é uma situação comum que acontecia frequentemente durante o desenvolvimento. Não houve nenhum problema com os designs antigos ou novos, eles mudaram naturalmente com os requisitos - como eu entendi, essa é uma vantagem do TDD, que o design evolui.
Conclusão
Obrigado por todas as respostas e discussões. No resumo desta discussão, pensei em uma abordagem que testarei no meu próximo projeto.
- Antes de tudo, escrevo todos os testes antes de implementar qualquer coisa como sempre fiz.
- Para requisitos, escrevo inicialmente alguns testes de aceitação que testam todo o programa. Depois, escrevo alguns testes de integração para os componentes nos quais preciso implementar o requisito. Se houver um componente que trabalhe em conjunto com outro componente para implementar esse requisito, eu também escreveria alguns testes de integração nos quais os dois componentes são testados juntos. Por último, mas não menos importante, se eu tiver que escrever um algoritmo ou qualquer outra classe com alta permutação - por exemplo, um serializador - eu escreveria testes de unidade para essas classes específicas. Todas as outras classes não são testadas, mas quaisquer testes de unidade.
- Para erros, o processo pode ser simplificado. Normalmente, um bug é causado por um ou dois componentes. Nesse caso, eu escreveria um teste de integração para os componentes que testam o bug. Se relacionado a um algoritmo, eu escreveria apenas um teste de unidade. Se não for fácil detectar o componente onde o erro ocorre, eu escreveria um teste de aceitação para localizar o erro - isso deve ser uma exceção.