Unidade testando várias condições em uma instrução IF

26

Eu tenho um pedaço de código que se parece com isso:

function bool PassesBusinessRules()
{
    bool meetsBusinessRules = false;

    if (PassesBusinessRule1 
         && PassesBusinessRule2
         && PassesBusinessRule3)
    {
         meetsBusinessRules= true;
    }

    return meetsBusinessRules;
}

Acredito que deve haver quatro testes de unidade para essa função específica. Três para testar cada uma das condições na instrução if e garantir que ela retorne false. E outro teste que garante que a função retorne verdadeira.

Pergunta: Na verdade, deveria haver dez testes de unidade? Nove que verifica cada um dos possíveis caminhos de falha. IE:

  • Falso Falso Falso
  • Falso Falso Verdadeiro
  • Falso Verdadeiro Falso

E assim por diante para cada combinação possível.

Eu acho que é um exagero, mas alguns dos outros membros da minha equipe não. A maneira como vejo é que, se BusinessRule1 falha, sempre deve retornar falso, não importa se foi verificado primeiro ou por último.

bwalk2895
fonte
O compilador usa avaliação gananciosa para o operador &&?
suszterpatt
12
Se você escrevesse 10 testes de unidade, estaria testando o operador&&, não seus métodos.
Mert Akcakaya 6/07/12
2
Não haveria apenas oito testes se você testasse todas as combinações possíveis? Três parâmetros booleanos ativados ou desativados.
Kris Harper
3
@Ert: Apenas se você puder garantir que o && sempre estará lá.
63012 Misko
Hickey: Se passamos escrevendo testes, é hora de não fazermos outra coisa. Cada um de nós precisa avaliar a melhor forma de gastar nosso tempo para maximizar nossos resultados, tanto em quantidade quanto em qualidade. Se as pessoas pensam que gastar cinquenta por cento do tempo escrevendo testes maximiza seus resultados - tudo bem para eles. Tenho certeza de que isso não é verdade para mim - prefiro gastar esse tempo pensando no meu problema. Estou certo de que, para mim, isso produz melhores soluções, com menos defeitos, do que qualquer outro uso do meu tempo. Um design ruim com um conjunto de testes completo ainda é um design ruim.
Job

Respostas:

29

Formalmente, esses tipos de cobertura têm nomes.

Primeiro, há uma cobertura de predicado : você deseja ter um caso de teste que torne a declaração if verdadeira e uma que a false. Ter essa cobertura atendida é provavelmente um requisito básico para um bom conjunto de testes.

Depois, há Cobertura de condição : Aqui você deseja testar se cada sub-condição no if tem o valor verdadeiro e falso. Obviamente, isso cria mais testes, mas geralmente detecta mais bugs; portanto, é uma boa ideia incluir no seu conjunto de testes se você tiver tempo.

Os critérios de cobertura mais avançados geralmente são chamados de Cobertura de condição combinatória : aqui o objetivo é ter um caso de teste que passe por todas as combinações possíveis de valores booleanos em seu teste.

Isso é melhor do que simples cobertura de predicado ou condição? Em termos de cobertura, é claro. Mas não é grátis. Ele tem um custo muito alto na manutenção de testes. Por esse motivo, a maioria das pessoas não se preocupa com a cobertura combinatória completa. Normalmente testar todos os ramos (ou todas as condições) será bom o suficiente para detectar bugs. A adição de testes extras para testes combinatórios não costuma detectar mais erros, mas exige muito esforço para criar e manter. O esforço extra geralmente faz com que isso não valha a recompensa muito pequena, então eu não recomendaria isso.

Parte desta decisão deve se basear em quão arriscado você acha que esse código será. Se houver muito espaço para falhar, vale a pena testar. Se for um pouco estável e não mudar muito, considere concentrar seus esforços de teste em outro lugar.

Oleksi
fonte
2
Se os valores booleanos são transmitidos de fontes externas (o que significa que nem sempre são validados), a cobertura condicional combinatória é frequentemente necessária. Primeiro faça uma tabela das combinações. Em seguida, para cada entrada, decida se essa entrada representa um caso de uso significativo. Caso contrário, deve haver código em algum lugar (asserções de software ou cláusula de validação) para impedir que essa combinação seja executada. É importante não agrupar todos os parâmetros em um único teste combinatório: tente particionar parâmetros em grupos que interagem entre si, ou seja, compartilha a mesma expressão booleana.
Rwong
Você tem certeza dos termos em negrito? Sua resposta parece ser a única ocorrência de "Cobertura de condição combinatória", e alguns recursos dizem que "cobertura predicada" e "cobertura condicional" são a mesma coisa.
Stijn
8

Por fim, depende de você (equipe), do código e do ambiente específico do projeto. Não há regra universal. Você (equipe r) deve escrever quantos testes precisar para sentir-se confortável de que o código esteja realmente correto . Portanto, se seus colegas de equipe não estiverem convencidos de quatro testes, talvez você precise de mais.

O tempo da OTOH para escrever testes de unidade geralmente é um recurso escasso. Portanto, lute para encontrar a melhor maneira de gastar o tempo limitado que você tem . Por exemplo, se você tiver outro método importante com cobertura de 0%, pode ser melhor escrever alguns testes de unidade para cobrir esse, em vez de adicionar testes extras para esse método. Obviamente, isso também depende de quão frágil é a implementação de cada um. Planejar muitas mudanças nesse método específico em um futuro próximo pode justificar uma cobertura extra de teste de unidade. Portanto, pode estar em um caminho crítico dentro do programa. Esses são todos os fatores que somente você (equipe r) pode avaliar.

Pessoalmente, normalmente ficaria feliz com os 4 testes que você descreve, ou seja:

  • verdadeiro falso falso
  • falso verdadeiro falso
  • falso falso verdadeiro
  • verdadeiro verdadeiro verdadeiro

mais talvez um:

  • verdadeiro verdadeiro falso

para garantir que a única maneira de obter um valor de retorno trueseja satisfazer todas as três regras de negócios. Mas, no final, se seus colegas de equipe insistirem em ter caminhos combinatórios cobertos, pode ser mais barato adicionar esses testes extras do que continuar o argumento por muito mais tempo :-)

Péter Török
fonte
3

Se você quiser estar seguro, precisará de oito testes de unidade usando as condições representadas por uma tabela verdade de três variáveis ​​( http://teach.valdosta.edu/plmoch/MATH4161/Spring%202004/and_or_if_files/image006.gif ).

Você nunca pode ter certeza de que a lógica de negócios sempre estipula que as verificações são executadas nessa ordem e que você deseja que o teste saiba o mínimo possível sobre a implementação real.

smp7d
fonte
2
O teste de unidade é um teste de caixa branca.
Péter Török
Fim bem não deve importar, && é communitive, ou pelo menos deveria ser
Zachary K
2

Sim, deve haver a combinação completa em um mundo ideal.

Ao fazer o teste de unidade, você realmente deve tentar ignorar como o método funciona. Simplesmente forneça as 3 entradas e verifique se a saída está correta.

Telastyn
fonte
11
O teste de unidade é um teste de caixa branca. E não vivemos em um mundo ideal.
Péter Török
@ PéterTörök - Não vivemos em um mundo ideal para ter certeza, mas a stackexchange discorda de você no outro ponto. Especialmente para TDD, os testes são gravados nas especificações, não na implementação. Pessoalmente, tomo 'especificação' para incluir todas as entradas (incluindo variáveis-membro) e todas as saídas (incluindo efeitos colaterais).
Telastyn
11
É apenas um thread específico no StackOverflow, sobre um caso específico, que não deve ser generalizado demais. Especialmente porque este post atual é obviamente sobre o código de teste que já está escrito.
Péter Török
1

Estado é mau. A função a seguir não precisa de um teste de unidade porque não tem efeitos colaterais e é bem entendido o que faz e o que não faz. Por que testá-lo? Você não confia em seu próprio cérebro ??? Funções estáticas são ótimas!

static function bool Foo(bool a, bool b, bool c)
{
    return a && b && c;
}
Trabalho
fonte
2
Não, eu não confio em meu próprio cérebro - aprendi da maneira mais difícil sempre verificar o que faço :-) Então, eu ainda precisaria de testes de unidade para garantir que eu não tenha digitado algo errado, e que ninguém vai para quebrar o código no futuro. E mais testes de unidade para verificar o método de chamada que calcula o estado representado por a, be c. Você pode mover a lógica de negócios da maneira que quiser, e no final ainda precisa testá-la em algum lugar.
Péter Török
@ Péter Török, você também pode digitar erros nos testes e, assim, acabar com falsos positivos, então onde você para? Você escreve testes de unidade para seus testes de unidade? Também não confio em meu cérebro 100%, mas no final das contas escrever código é o que faço para viver. É possível ter um bug dentro dessa função, mas é importante escrever o código de tal maneira que seja fácil rastrear um bug na origem e, assim que você isolar o problema e corrigir o problema, será melhor . Código bem escrito pode contar com a integração testa principalmente infoq.com/presentations/Simple-Made-Easy
2
De fato, os testes também podem estar com defeito. (O TDD resolve isso fazendo com que os testes falhem primeiro.) No entanto, cometer o mesmo tipo de erro duas vezes (e ignorá-lo) tem uma probabilidade muito menor. Em geral, nenhuma quantidade e tipo de teste pode provar que o software está livre de erros, apenas reduza a probabilidade de erros para um nível aceitável. E na velocidade de rastreamento bugs para a fonte, IMO nada pode bater testes de unidade - rulez rápido feedback :-)
Péter Török
"A função a seguir não precisa de um teste de unidade" Acho que você está sendo sarcástico aqui, mas não está claro. Eu confio no meu próprio cérebro? NÃO! Confio no cérebro do próximo cara que toca no código? AINDA MAIS NÃO! Confio que todas as suposições por trás do código serão verdadeiras daqui a um ano? ... você entende o meu desvio. Além disso, funções estáticas eliminam OO ... se você deseja fazer FP, use uma linguagem FP.
Rob
1

Eu sei que esta pergunta é bastante antiga. Mas quero dar outra perspectiva para o problema.

Primeiro, seus testes de unidade devem ter dois propósitos:

  1. Crie uma documentação para você e seus companheiros de equipe. Assim, após um determinado período de tempo, você poderá ler o teste de unidade e certificar-se de entender what's the class' intentionehow the class is doing its work
  2. Durante o desenvolvimento, o teste de unidade garante que o código que estamos escrevendo esteja realizando seu trabalho conforme planejado em nossa mente.

Então, recapitulando o problema, queremos testar a complex if statement, para o exemplo dado, existem 2 ^ 3 possibilidades, que é uma quantidade importante de testes que podemos escrever.

  • Você pode se adaptar a esse fato e anotar 8 testes ou fazer uso de testes parametrizados
  • Você também pode seguir as outras respostas e lembrar que os testes devem ser claros com a intenção. Dessa forma, não vamos mexer com muitos detalhes que, no futuro próximo, podem ser mais difíceis de entender what is doing the code

Por outro lado, se você está na posição de que seus testes são ainda mais complexos que a implementação, é porque a implementação deve ser reprojetada (mais ou menos, dependendo do caso), e não o próprio teste.

Para as complexas declarações if, por exemplo, você pode pensar no padrão de responsabilidade da cadeia , implementando cada manipulador da seguinte maneira:

If some simple business rule apply, derive to the next handler

Quão simples seria testar várias regras simples, em vez de uma regra complexa?

Espero que ajude,

cnexans
fonte
0

Este é um daqueles casos em que Algo como verificação rápida ( http://en.wikipedia.org/wiki/QuickCheck ) será seu amigo. Em vez de escrever todos os N casos manualmente, faça com que o computador gere todos (ou pelo menos um grande número) de possíveis casos de teste e valide se todos retornam um resultado sensato.

Nós programamos computadores para viver aqui, por que não programar o computador para gerar seus casos de teste para você?

Zachary K
fonte
0

Você pode refatorar as condições em condições de guarda:

if (! PassesBusinessRule1) {
    return false;
}

if (! PassesBusinessRule2) {
    return false;
}

if (! PassesBusinessRule3) {
    return false;
}

Não acho que isso reduz o número de casos, mas minha experiência é que é mais fácil divulgá-los dessa maneira.

(Observe que sou um grande fã do "único ponto de saída", mas faço uma exceção para condições de guarda. Mas existem outras maneiras de estruturar o código para que você não tenha retornos separados.)

Roubar
fonte