Testando uma lista ... Tudo no mesmo teste ou em um teste para cada condição?

21

Estou testando que uma função faz o que se espera em uma lista. Então eu quero testar

f(null) -> null
f(empty) -> empty
f(list with one element) -> list with one element
f(list with 2+ elements) -> list with the same number of elements, doing what expected

Para fazer isso, qual é a melhor abordagem?

  • Testando todos os casos no mesmo teste (método), sob o nome "WorksAsExpected"
  • Colocando um teste para cada caso, tendo assim
    • "WorksAsExpectedWhenNull"
    • "WorksAsExpectedWhenEmpty"
    • "WorksAsExpectedWhenSingleElement"
    • "WorksAsExpectedWhenMoreElements"
  • Outra opção que eu não estava pensando :-)
malarres
fonte
2
Eu os escreveria como casos de teste separados. Você pode usar testes parametrizados se o seu sistema de teste suportar isso.
precisa saber é o seguinte
5
Se você escrever seus testes em um determinado ... Quando ... estilo, então, em seguida, torna-se evidente que eles devem realmente ser testados separadamente ...
Robbie Dee
1
Gostaria de acrescentar: IMO, é bom separar casos extremos (como nulos e vazios) em testes separados, porque geralmente envolvem lógica de casos especiais em diferentes implementações possíveis e, se esses testes falharem, eles indicarão claramente em de que maneira o código sob teste falha (você não precisará se aprofundar mais ou depurar o caso de teste para descobrir o que está acontecendo).
Filip Milovanović
1
Listar com elementos duplicados?
Atayenel

Respostas:

30

A regra geral simples que eu uso para executar um conjunto de testes em um caso de teste ou em muitos é: envolve apenas uma configuração?

Portanto, se eu estivesse testando que, para vários elementos, ele processou todos eles e obteve o resultado correto, posso ter duas ou mais declarações, mas preciso configurar a lista apenas uma vez. Então, um caso de teste está bom.

No seu caso, porém, eu teria que configurar uma lista nula, uma lista vazia etc. Isso é várias configurações. Então, eu definitivamente criaria vários testes neste caso.

Como outros já mencionaram, esses "múltiplos testes" podem existir como um único caso de teste parametrizado; ou seja, o mesmo caso de teste é executado em uma variedade de dados de configuração. A chave para saber se essa é uma solução viável está nas outras partes do teste: "ação" e "afirmação". Se você pode executar as mesmas ações e afirmações em cada conjunto de dados, use esta abordagem. Se você se encontra adicionando ifs, por exemplo, para executar código diferente em diferentes partes desses dados, essa não é a solução. Use casos de teste individuais nesse último caso.

David Arno
fonte
14

Há uma troca. Quanto mais você empacotar em um teste, maior será a probabilidade de você ter um efeito de cebola tentando fazê-lo passar. Em outras palavras, a primeira falha interrompe esse teste. Você não saberá sobre as outras asserções até corrigir a primeira falha. Dito isto, ter vários testes de unidade que são basicamente similares, exceto pelo código de configuração, é muito trabalhoso apenas para descobrir que alguns funcionam como escritos e outros não.

Ferramentas possíveis, com base na sua estrutura:

  • Teorias . Uma teoria permite testar uma série de fatos sobre um conjunto de dados. A estrutura alimentará seus testes com vários cenários de dados de teste - por um campo ou por um método estático que gera os dados. Se alguns de seus fatos se aplicam com base em algumas condições prévias e outras, essas estruturas não introduzem o conceito de suposição . Você Assume.that()simplesmente pula o teste para os dados, se eles falharem na pré-condição. Isso permite definir "Funciona conforme o esperado" e, em seguida, simplesmente alimentar muitos dados. Ao visualizar os resultados, você tem uma entrada para os testes pai e, em seguida, uma subentrada para cada parte dos dados.
  • Testes parametrizados . Os testes parametrizados foram um precursor das teorias; portanto, pode não haver essa verificação prévia que você pode ter com as teorias. O resultado final é o mesmo. Você visualiza os resultados, possui uma entrada pai para o próprio teste e, em seguida, uma entrada específica para cada ponto de dados.
  • Um teste com várias afirmações . Demora menos tempo para fazer a configuração, mas você acaba descobrindo problemas um pouco de cada vez. Se o teste for muito longo e houver muitos cenários diferentes testados, há dois grandes riscos: levará muito tempo para ser executado e sua equipe ficará cansada dele e desligará o teste.
  • Vários testes com implementação semelhante . É importante observar que, se as asserções forem diferentes, os testes não serão sobrepostos. No entanto, essa seria a sabedoria convencional de uma equipe focada no TDD.

Não sou da opinião estrita de que só pode haver uma assertdeclaração em seu teste, mas coloco as restrições de que todas as afirmações devem testar as pós-condições de uma única ação. Se a única diferença entre os testes é de dados, sou da opinião de usar os recursos mais avançados de testes controlados por dados, como testes parametrizados ou teorias.

Pese suas opções para decidir qual é o melhor resultado. Eu direi que "WorksAsExpectedWhenNull" é fundamentalmente diferente de qualquer um dos casos em que você está lidando com uma coleção que possui vários números de elementos.

Berin Loritsch
fonte
5

Esses são casos de teste diferentes, mas o código para o teste é o mesmo. Usar testes parametrizados é, portanto, a melhor solução. Se sua estrutura de teste não suportar parametrização, extraia o código compartilhado em uma função auxiliar e chame-o de casos de teste individuais.

Tente evitar a parametrização por meio de um loop em um caso de teste, pois isso dificulta a determinação de qual conjunto de dados causou o erro.

No seu ciclo de refatoramento TDD vermelho, verde, você deve adicionar um conjunto de dados de exemplo por vez. A combinação de vários casos de teste em um teste parametrizado faria parte da etapa de refatoração.

Uma abordagem bastante diferente é o teste de propriedades . Você criaria vários testes (parametrizados) que afirmam várias propriedades de sua função, sem especificar dados de entrada concretos. Por exemplo, uma propriedade pode ser: para todas as listas xs, a lista ys = f(xs)tem o mesmo tamanho que xs. A estrutura de teste geraria listas interessantes e listas aleatórias e afirmaria que suas propriedades são válidas para todas elas. Isso se afasta da especificação manual de exemplos, pois a escolha manual de exemplos pode perder casos interessantes.

amon
fonte
A "falta" na última frase não deveria ser "encontrar"?
Robbie Dee
@RobbieDee O inglês é ambíguo, fixo.
amon
3

Ter um teste para cada caso é apropriado porque testar um único conceito em cada teste é uma boa diretriz que é frequentemente recomendada.

Veja este post: Não há problema em ter várias afirmações em um único teste de unidade? . Também há uma discussão relevante e detalhada:

Minha orientação é geralmente que você teste um CONCEITO lógico por teste. você pode ter várias declarações no mesmo objeto. eles geralmente serão o mesmo conceito sendo testado. Fonte - Roy Osherove

[...]

Os testes devem falhar por apenas um motivo, mas isso nem sempre significa que deve haver apenas uma instrução Assert. IMHO é mais importante manter o padrão "Organizar, Agir, Afirmar".

A chave é que você tem apenas uma ação e, em seguida, inspeciona os resultados dessa ação usando afirmações. Mas é "Organizar, agir, afirmar, final de teste". Se você estiver tentado a continuar o teste executando outra ação e mais declarações depois, faça um teste separado. Fonte

filho
fonte
0

Na minha opinião, isso depende da condição do teste.

  • Se o seu teste tiver apenas 1 condição para configurá-lo, mas muitos efeitos colaterais. A afirmação múltipla é aceitável.
  • Mas quando você tem várias condições, significa que você tem vários casos de teste, cada um deve ser coberto apenas por 1 teste de unidade.
HungDL
fonte
isto se parece mais com um comentário, consulte Como responder
gnat