Estou trabalhando em um comparador de lista para ajudar a classificar uma lista não ordenada de resultados de pesquisa por requisitos muito específicos de nosso cliente. Os requisitos exigem um algoritmo de relevância classificada com as seguintes regras em ordem de importância:
- Correspondência exata no nome
- Todas as palavras de consulta de pesquisa em nome ou sinônimo do resultado
- Algumas palavras de consulta de pesquisa em nome ou sinônimo do resultado (% decrescente)
- Todas as palavras da consulta de pesquisa na descrição
- Algumas palavras da consulta de pesquisa na descrição (% decrescente)
- Data da última modificação decrescente
A escolha do design natural para esse comparador parecia ser uma classificação pontuada com base em potências de 2. A soma de regras menos importantes nunca pode ser mais do que uma correspondência positiva em uma regra de maior importância. Isso é alcançado pela seguinte pontuação:
- 32.
- 16
- 8 (Pontuação secundária do desempate com base em% descendente)
- 4
- 2 (Pontuação do desempate secundário com base em% descendente)
- 1
No espírito do TDD, decidi começar com meus testes de unidade primeiro. Ter um caso de teste para cada cenário único seria no mínimo 63 casos de teste exclusivos, sem considerar casos de teste adicionais para a lógica secundária do desempatador nas regras 3 e 5. Isso parece arrogante.
Os testes reais serão realmente menores. Com base nas próprias regras reais, certas regras garantem que as regras mais baixas sempre sejam verdadeiras (por exemplo, quando 'Todas as palavras da consulta de pesquisa aparecerem na descrição', então regra 'Algumas palavras da consulta de pesquisa aparecerão na descrição' sempre será verdadeira). Ainda vale o nível de esforço em escrever cada um desses casos de teste? Esse é o nível de teste normalmente exigido quando se fala em 100% de cobertura de teste no TDD? Caso contrário, qual seria uma estratégia de teste alternativa aceitável?
fonte
Respostas:
Sua pergunta implica que o TDD tem algo a ver com "escrever todos os casos de teste primeiro". IMHO que não está "no espírito do TDD", na verdade é contra . Lembre-se de que TDD significa " desenvolvimento orientado a teste "; portanto, você precisa apenas dos casos de teste que realmente "orientam" sua implementação, e não mais. E desde que sua implementação não seja projetada de maneira a aumentar exponencialmente o número de blocos de código a cada novo requisito, você também não precisará de um número exponencial de casos de teste. No seu exemplo, o ciclo TDD provavelmente terá a seguinte aparência:
Em seguida, comece com o segundo requisito:
Aqui está o problema : quando você adiciona casos de teste para o número de requisito / categoria "n", você só precisará adicionar testes para garantir que a pontuação da categoria "n-1" seja maior que a pontuação da categoria "n" . Você não precisará adicionar nenhum caso de teste para todas as outras combinações das categorias 1, ..., n-1, pois os testes que você escreveu anteriormente garantirão que as pontuações dessas categorias ainda estejam na ordem correta.
Portanto, isso fornecerá vários casos de teste que crescem aproximadamente lineares com o número de requisitos, e não exponencialmente.
fonte
Considere escrever uma classe que passa por uma lista predefinida de condições e multiplica uma pontuação atual por 2 para cada verificação bem-sucedida.
Isso pode ser testado com muita facilidade, usando apenas alguns testes simulados.
Em seguida, você pode escrever uma classe para cada condição e existem apenas 2 testes para cada caso.
Não estou realmente entendendo seu caso de uso, mas espero que este exemplo ajude.
Você notará que seus testes de 2 ^ condições se reduzem rapidamente a 4+ (2 * condições). 20 é muito menos arrogante que 64. E se você adicionar outra mais tarde, não precisará alterar QUALQUER das classes existentes (princípio aberto-fechado), para que não precise escrever 64 novos testes, apenas terá para adicionar outra classe com 2 novos testes e injetar isso na sua classe ScoreBuilder.
fonte
Você precisará definir "vale a pena". O problema com esse tipo de cenário é que os testes terão um retorno decrescente da utilidade. Certamente o primeiro teste que você escrever valerá totalmente a pena. Ele pode encontrar erros óbvios na prioridade e até coisas como analisar erros ao tentar dividir as palavras.
O segundo teste valerá a pena porque cobre um caminho diferente através do código, provavelmente verificando outra relação de prioridade.
O 63º teste provavelmente não valerá a pena, porque é algo que você tem 99,99% de confiança, coberto pela lógica do seu código ou de outro teste.
Meu entendimento é de 100% de cobertura significa que todos os caminhos de código são exercidos. Isso não significa que você faça todas as combinações de suas regras, mas todos os diferentes caminhos que seu código pode seguir (como você indica, algumas combinações não podem existir no código). Mas como você está fazendo TDD, ainda não há "código" para verificar os caminhos. A letra do processo diria make all 63+.
Pessoalmente, acho que 100% de cobertura é um sonho. Além disso, não é prático. Existem testes de unidade para atendê-lo, não vice-versa. À medida que você faz mais testes, você obtém retornos decrescentes do benefício (a probabilidade de o teste impedir um bug + a confiança de que o código está correto). Dependendo do que o seu código faz, define onde, nessa escala móvel, você para de fazer testes. Se o seu código estiver executando um reator nuclear, talvez todos os 63 ou mais testes valham a pena. Se o seu código estiver organizando seu arquivo de música, provavelmente você poderá se safar muito menos.
fonte
Eu diria que este é um caso perfeito para TDD.
Você tem um conjunto conhecido de critérios para testar, com uma análise lógica desses casos. Supondo que você irá testá-los agora ou mais tarde, parece fazer sentido pegar o resultado conhecido e construí-lo, assegurando que você esteja, de fato, cobrindo cada uma das regras de forma independente.
Além disso, você descobrirá se adicionar uma nova regra de pesquisa quebra uma regra existente. Se você fizer tudo isso no final da codificação, você provavelmente corre um risco maior de alterar um para corrigir um, que quebra outro, que quebra outro ... E você aprende ao implementar as regras se seu design é válido ou precisa de ajustes.
fonte
Não sou fã de interpretar estritamente 100% da cobertura de teste como escrever especificações contra cada método único ou testar todas as permutações do código. Fazer isso fanaticamente tende a levar a um design de teste de suas classes que não encapsule adequadamente a lógica de negócios e produz testes / especificações que geralmente não fazem sentido em termos de descrição da lógica de negócios suportada. Em vez disso, concentro-me na estruturação dos testes, da mesma forma que as regras de negócios, e me esforço para exercitar cada ramo condicional do código com testes com a expectativa explícita de que esses testes são facilmente entendidos pelo testador como geralmente seriam casos de uso e, na verdade, descrevem o regras de negócios que foram implementadas.
Com essa ideia em mente, eu testaria exaustivamente os 6 fatores de classificação que você listou isoladamente, seguidos por 2 ou 3 testes de estilo de integração que garantem que você esteja agregando seus resultados aos valores esperados de classificação geral. Por exemplo, no caso 1, Correspondência exata do nome, eu teria pelo menos dois testes de unidade para testar quando é exato e quando não é e se os dois cenários retornam a pontuação esperada. Se for sensível a maiúsculas e minúsculas, também um caso para testar "Correspondência exata" vs. "correspondência exata" e possivelmente outras variações de entrada, como pontuação, espaços extras, etc. também retornam as pontuações esperadas.
Depois de trabalhar com todos os fatores individuais que contribuem para as pontuações no ranking, suponho que eles funcionem corretamente no nível de integração e me concentro em garantir que seus fatores combinados contribuam corretamente para a pontuação final esperada.
Supondo que os casos # 2 / # 3 e # 4 / # 5 sejam generalizados para os mesmos métodos subjacentes, mas, ao passar campos diferentes, você só precisará escrever um conjunto de testes de unidade para os métodos subjacentes e escrever testes de unidade adicionais simples para testar os requisitos específicos. campos (título, nome, descrição etc.) e pontuação no fatorial designado, portanto, isso reduz ainda mais a redundância do seu esforço geral de teste.
Com essa abordagem, a abordagem descrita acima provavelmente produziria 3 ou 4 testes de unidade no caso 1, talvez 10 especificações sobre alguns / todos os sinônimos / responsáveis - mais 4 especificações sobre a pontuação correta dos casos 2 - 5 e 2 até 3 especificações na classificação final ordenada por data e, em seguida, 3 a 4 testes de nível de integração que medem todos os 6 casos combinados de maneiras prováveis (esqueça os casos de borda obscuros por enquanto, a menos que você veja claramente um problema no seu código que precise ser exercido para garantir essa condição é tratada) ou garantir que não seja violado / quebrado por revisões posteriores. Isso gera cerca de 25 especificações para exercitar 100% do código escrito (mesmo que você não tenha chamado diretamente 100% dos métodos escritos).
fonte
Eu nunca fui fã de 100% de cobertura de teste. Na minha experiência, se algo é simples o suficiente para testar com apenas um ou dois casos de teste, é simples o suficiente para raramente falhar. Quando falha, geralmente é devido a alterações na arquitetura que exigiriam alterações de teste de qualquer maneira.
Dito isto, para requisitos como o seu, eu sempre teste minuciosamente, mesmo em projetos pessoais onde ninguém está me fazendo, porque esses são os casos em que o teste unitário economiza tempo e agravamento. Quanto mais testes de unidade forem necessários para testar algo, mais tempo os testes de unidade economizarão.
Isso porque você só pode segurar tantas coisas na cabeça de uma só vez. Se você está tentando escrever um código que funcione para 63 combinações diferentes, geralmente é difícil corrigir uma combinação sem quebrar outra. Você acaba testando manualmente outras combinações repetidamente. O teste manual é muito mais lento, o que faz com que você não queira executar novamente todas as combinações possíveis toda vez que fizer uma alteração. Com isso, é mais provável que você perca alguma coisa e perca mais tempo buscando caminhos que não funcionam em todos os casos.
Além do tempo economizado comparado ao teste manual, há muito menos tensão mental, o que facilita o foco no problema em questão, sem se preocupar em introduzir regressões acidentalmente. Isso permite que você trabalhe mais rápido e mais tempo sem queimar. Na minha opinião, os benefícios de saúde mental por si só valem o custo do teste de código complexo, mesmo que não tenha poupado tempo.
fonte