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.
fonte
Respostas:
Está comparando laranjas e maçãs.
Testes de integração, testes de aceitação, testes de unidade, testes de comportamento - todos são testes e ajudarão você a melhorar seu código, mas também são bem diferentes.
Vou examinar cada um dos diferentes testes na minha opinião e, esperançosamente, explicar por que você precisa de uma mistura de todos eles:
Testes de integração:
Simplesmente, teste se diferentes componentes de seu sistema se integram corretamente - por exemplo - talvez você simule uma solicitação de serviço da web e verifique se o resultado volta. Geralmente, eu usaria dados estáticos reais (ish) e dependências simuladas para garantir que eles possam ser verificados consistentemente.
Testes de aptidão:
Um teste de aceitação deve se correlacionar diretamente com um caso de uso de negócios. Pode ser enorme ("as transações são enviadas corretamente") ou pequenas ("o filtro filtra com êxito uma lista") - não importa; o que importa é que ele deve estar explicitamente vinculado a um requisito específico do usuário. Eu gosto de focar neles para o desenvolvimento orientado a testes, porque significa que temos um bom manual de referência de testes para histórias de usuários para dev e qa verificar.
Testes unitários:
Para pequenas unidades discretas de funcionalidade que podem ou não compor uma história de usuário individual por si só - por exemplo, uma história de usuário que diz que recuperamos todos os clientes quando acessamos uma página da web específica pode ser um teste de aceitação (simule acessar a web página e verificação da resposta), mas também pode conter vários testes de unidade (verifique se as permissões de segurança estão verificadas, verifique se a conexão com o banco de dados consulta corretamente, verifique se qualquer código que limita o número de resultados é executado corretamente) - todos esses são "testes de unidade" que não é um teste de aceitação completo.
Testes de comportamento:
Defina qual deve ser o fluxo de um aplicativo no caso de uma entrada específica. Por exemplo, "quando a conexão não puder ser estabelecida, verifique se o sistema tenta novamente a conexão". Novamente, é improvável que seja um teste de aceitação total, mas ainda permite que você verifique algo útil.
Tudo isso é, na minha opinião, através de muita experiência em escrever testes; Não gosto de me concentrar nas abordagens dos livros didáticos - em vez disso, concentre-se no que dá valor aos seus testes.
fonte
TL; DR: Desde que atenda às suas necessidades, sim.
Estou desenvolvendo o ATDD (Acceptance Test Driven Development) há muitos anos. Pode ser muito bem sucedido. Há algumas coisas para estar ciente.
Agora os benefícios
Como sempre, cabe a você fazer a análise e descobrir se essa prática é apropriada para sua situação. Ao contrário de muitas pessoas, não acho que haja uma resposta certa idealizada. Depende de suas necessidades e exigências.
fonte
Os testes de unidade funcionam melhor quando a interface pública dos componentes para os quais são usados não muda com muita frequência. Isso significa que, quando os componentes já foram bem projetados (por exemplo, seguindo os princípios do SOLID).
Portanto, acreditar que um bom design apenas "evolui" de "executar" muitos testes de unidade em um componente é uma falácia. O TDD não é um "professor" para um bom design, ele só pode ajudar um pouco para verificar se certos aspectos do design são bons (especialmente para testabilidade).
Quando seus requisitos mudam e você precisa alterar as partes internas de um componente, isso interromperá 90% de seus testes de unidade; portanto, é necessário refatorá-los com muita frequência, pois o design provavelmente não foi tão bom.
Portanto, meu conselho é: pense no design dos componentes que você criou e como torná-los mais seguindo o princípio de abertura / fechamento. A idéia do último é garantir que a funcionalidade de seus componentes possa ser estendida posteriormente sem alterá-los (e, portanto, sem quebrar a API do componente usada pelos testes de unidade). Esses componentes podem (e devem ser) cobertos por testes de teste de unidade, e a experiência não deve ser tão dolorosa quanto você a descreveu.
Quando você não pode criar esse projeto imediatamente, os testes de aceitação e integração podem realmente ser um começo melhor.
EDIT: Às vezes, o design de seus componentes pode ser bom, mas o design dos testes de unidade pode causar problemas . Exemplo simples: você deseja testar o método "MyMethod" da classe X e escrever
(suponha que os valores tenham algum tipo de significado).
Suponha ainda que no código de produção haja apenas uma chamada
X.MyMethod
. Agora, para um novo requisito, o método "MyMethod" precisa de um parâmetro adicional (por exemplo, algo comocontext
), que não pode ser omitido. Sem testes de unidade, seria necessário refatorar o código de chamada em apenas um lugar. Com testes de unidade, é preciso refatorar 500 lugares.Mas a causa aqui não é o teste de unidade, é apenas o fato de que a mesma chamada para "X.MyMethod" é repetida várias vezes, não seguindo estritamente o princípio "Não se repita (DRY). Portanto, a solução aqui é colocar os dados de teste e os valores esperados relacionados em uma lista e executar as chamadas para "MyMethod" em um loop (ou, se a ferramenta de teste suportar os chamados "testes de unidade de dados", para usar esse recurso). o número de lugares a serem alterados nos testes de unidade quando a assinatura do método muda para 1 (em oposição a 500).
No seu caso no mundo real, a situação pode ser mais complexa, mas espero que você entenda. Quando seus testes de unidade usam uma API de componentes para a qual você não sabe se pode estar sujeito a alterações, reduza o número de chamadas para essa API no mínimo.
fonte
X x= new X(); AssertTrue(x.MyMethod(12,"abc"))
antes de realmente implementar o método. Usando o design inicial, você pode escreverclass X{ public bool MyMethod(int p, string q){/*...*/}}
primeiro e depois os testes. Nos dois casos, você tomou a mesma decisão de design. Se a decisão foi boa ou ruim, o TDD não informará.Sim, claro que é.
Considere isto:
Veja a diferença geral ....
O problema é de cobertura de código; se você pode realizar um teste completo de todo o seu código usando testes de integração / aceitação, não há problema. Seu código foi testado. Esse é o objetivo.
Eu acho que você pode precisar misturá-los, pois todo projeto baseado em TDD exigirá alguns testes de integração apenas para garantir que todas as unidades funcionem bem juntas (sei por experiência própria que uma base de código testada a 100% da unidade não necessariamente funciona quando você coloca todos juntos!)
O problema realmente se resume à facilidade de testar, depurar as falhas e corrigi-las. Algumas pessoas acham que seus testes de unidade são muito bons nisso, são pequenos e simples e as falhas são fáceis de ver, mas a desvantagem é que você precisa reorganizar seu código para se adequar às ferramentas de teste de unidade e escrever muitos deles. É mais difícil escrever um teste de integração para cobrir muito código, e você provavelmente precisará usar técnicas como o log para depurar falhas (no entanto, eu diria que é necessário fazer isso de qualquer maneira, não é possível testar falhas na unidade) quando estiver no local!).
De qualquer forma, você ainda recebe o código testado, só precisa decidir qual mecanismo combina melhor com você. (Eu usaria um pouco de mix, testaria os algoritmos complexos e integraria o resto).
fonte
Eu acho que é uma ideia horrível.
Como os testes de aceitação e o teste de integração tocam partes mais amplas do seu código para testar um destino específico, eles precisarão de mais refatoração ao longo do tempo, e não menos. Pior ainda, como cobrem amplas seções do código, aumentam o tempo que você gasta rastreando a causa raiz, já que você tem uma área mais ampla para pesquisar.
Não, você deve escrever mais testes de unidade, a menos que tenha um aplicativo ímpar que seja 90% da interface do usuário ou algo mais estranho para o teste de unidade. A dor que você está enfrentando não é dos testes de unidade, mas do desenvolvimento do primeiro teste. Geralmente, você deve gastar apenas 1/3 do seu tempo na maioria dos testes de escrita. Afinal, eles estão lá para atendê-lo, não vice-versa.
fonte
A "vitória" do TDD é que, uma vez que os testes foram escritos, eles podem ser automatizados. O outro lado é que ele pode consumir uma parte significativa do tempo de desenvolvimento. Se isso realmente atrasa todo o processo é discutível. O argumento é que o teste inicial reduz o número de erros a serem corrigidos no final do ciclo de desenvolvimento.
É aqui que o BDD entra, já que os comportamentos podem ser incluídos no teste de unidade, para que o processo seja, por definição, menos abstrato e mais tangível.
Claramente, se uma quantidade infinita de tempo estivesse disponível, você faria o maior número possível de testes de várias variedades. No entanto, o tempo é geralmente limitado e os testes contínuos são rentáveis até certo ponto.
Tudo isso leva à conclusão de que os testes que fornecem mais valor devem estar na frente do processo. Isso, por si só, não favorece automaticamente um tipo de teste em detrimento de outro - mais que cada caso deve ser considerado por seus méritos.
Se você estiver escrevendo um widget de linha de comando para uso pessoal, estará interessado principalmente em testes de unidade. Enquanto um serviço da web, por exemplo, exigiria uma quantidade substancial de integração / teste comportamental.
Embora a maioria dos tipos de teste se concentre no que poderia ser chamado de "linha de corrida", ou seja, testando o que é exigido pelas empresas hoje em dia, o teste de unidade é excelente para eliminar erros sutis que podem surgir em fases posteriores de desenvolvimento. Como esse é um benefício que não pode ser facilmente medido, geralmente é ignorado.
fonte
Este é o ponto chave, e não apenas "a última vantagem". Quando o projeto se torna cada vez maior, seus testes de aceitação de integração estão se tornando cada vez mais lentos. E aqui, eu quero dizer tão lento que você vai parar de executá-los.
Obviamente, os testes de unidade também estão se tornando mais lentos, mas ainda são mais rápidos do que a ordem de magnitude. Por exemplo, no meu projeto anterior (c ++, cerca de 600 kLOC, 4000 testes de unidade e 200 testes de integração), levou cerca de um minuto para executar todos e mais de 15 para executar testes de integração. Para criar e executar testes de unidade para a peça que está sendo alterada, levaria menos de 30 segundos, em média. Quando você pode fazê-lo tão rápido, você desejará fazê-lo o tempo todo.
Só para esclarecer: não digo para não adicionar testes de integração e aceitação, mas parece que você fez o TDD / BDD de maneira errada.
Sim, projetar com a testabilidade em mente tornará o design melhor.
Bem, quando os requisitos mudam, você precisa alterar o código. Eu diria que você não terminou seu trabalho se não escreveu testes de unidade. Mas isso não significa que você deve ter 100% de cobertura com testes de unidade - esse não é o objetivo. Algumas coisas (como GUI, ou acessar um arquivo, ...) nem sequer são feitas para serem testadas em unidade.
O resultado disso é uma melhor qualidade do código e outra camada de teste. Eu diria que vale a pena.
Também tivemos vários testes de aceitação de 1000, e levaria uma semana inteira para executar todos.
fonte