Como devo testar a função booleana com muitas permutações possíveis?

4

Quando eu escrevo um teste de unidade eu costumo fornecer um contexto (objeto simples ou objeto ridicularizado / stubbed) que eu configurei de algumas maneiras e, em seguida, posso executar assert declaração no contexto:

nota: o código está no pseudo-código; groovy como sintaxe:

test myTest() {
  def o = getTestContext();
  o.string = "testme"
  o.number = "2"
  assert o.mult() == "testme testme" 
}

Mas como organizar o teste quando você precisa testar uma expressão booleana complexa que usa muitos parâmetros?

EDIT: Eu substituí a expressão de uma linha por algo mais legível para evitar confusão.

//this is not a real class, this is an example. Naming is bad, for conciseness sake
//the expression is coming from randomness realm, so it is probably refactorable and simplifiable, but complex real world expression still exists.
enum Type {X,Y,Z}
class C {
  boolean a,b,c,d;
  Type t;


  boolean isEnabled(boolean anotherFlag) {

    def condition1 = (a || b)
    def condition2 = (c && d)
    def goodType1 = t == X || t == Y
    def goodType2 = anotherFlag && t == Z || t == Z && !condition1 

    return (  condition1  || condition2 ) && (goodType1 || goodType2) 
  }
}

Todos os testes para este tipo de métodos que li até agora são muito verbosos, não completos e difíceis de entender.

E é uma pena que uma linha tão pequena de código, mesmo que seja 'complexa', gere testes terríveis.

Eu tentei quebrar a expressão booleana em sub métodos menores, mas às vezes isso não é tão conveniente e a contagem de permutação ainda é alta. Eu também costumo quebrar a expressão em variáveis intermediárias, mas isso não está ajudando no mundo de testes unitários ...

Como devo testar algo como isto para ter um código de teste que corresponda à brevidade do código testado e à integridade que deve assegurar que meu código funcione como esperado?

Editar: sobre a solução retida.

A refatoração é realmente um caminho a percorrer, mas eu realmente não quero "desenrolar" todas as combinações manualmente em meus testes: é detalhado, feio e difícil de entender.

No entanto, vou manter esta resposta como um passo preliminar obrigatório: quebrar a expressão em pedaços menores antes de qualquer outra coisa.

Uma vez que a refatoração é feita e o teste ainda resulta em testes combinatórios, então eu vou usar a solução TruthTable proposta. Vou apenas gerar as combinações e não declarar tudo.

Eu encontrei este artigo interessante, mas desatualizado, sobre a combinação de testes no Groovy.
A ferramenta que eles falaram está fora do radar do mecanismo de busca, por isso deve estar morto!
No entanto, vou usar o mesmo padrão:

assertThat(permutations, expectations, instanceObject)

Onde

  • permutação é todos os valores possíveis para atribuir propriedades em um "mapa condensado": (a: [verdadeiro, falso], b: [verdadeiro, falso], ...)
  • expectativa é todas as combinações que retornam um valor específico, todas as outras combinações serão verificadas em relação a um valor padrão.
por Guillaume 27.06.2014 / 11:36
fonte

3 respostas

11

Uma maneira bastante intuitiva de lidar com isso é codificar uma tabela de verdade no seu teste para que você tenha algo como:

//last in tuple is expected result, rest are inputs
test date = new List<Tuple<bool,bool,bool,bool,string>>()
{
   {true,true,true,true,"foo"}
   {true,true,true,false,"bar"}
   etc...
}

i.e. um teste orientado por dados. Para a tabela grande, você pode mover isso para um arquivo de dados que você absorve. O teste em si simplesmente precisa iterar sobre a tabela e conectar as entradas e verificar a saída esperada.

Isso também sugere uma possível refatoração do código. Uma entrada de Tuple<bool,bool> é essencialmente igual a um enum com 4 valores possíveis.

    
por 27.06.2014 / 12:08
fonte
10

Seu erro é assumir que o código é curto. O fato de ser um one-liner não significa que seja simples de testar, depurar e manter. Se eu tivesse que manter este código, eu teria WTFed bastante sobre pessoas que gostam de escrever código condensado, enigmático (também, eu espero que suas variáveis a , b etc. sejam apenas para um exemplo, e na vida real, você usa nomes mais significativos).

Veja o que parece quando refatorada, isSomething , isAlso e isSomethingElse devem ser substituídos por nomes significativos que documentam seu código:

boolean isSomething() {
    return (a || b) || (c && d)
}

boolean isAlso() {
    return t == X || t == Y
}

boolean isSomethingElse(boolean anotherFlag) {
    return anotherFlag && t == Z && a
}

boolean isEnabled(boolean anotherFlag) {
    if isSomething() && isAlso() {
        return true
    }

    return isSomethingElse(anotherFlag)
}

Este código é muito mais simples do que o one-liner, mas também torna mais claro que você precisará de mais de dois ou três testes unitários para testá-lo.

Número de permutações

Falando sobre o alto número de permutações que você esperaria:

return a

precisa de dois testes : um em que a instrução retorna true e outra onde ela retorna false.

return a && b

precisa de três ou quatro testes , dependendo do idioma. A maioria dos idiomas seria preguiçosa e evite avaliar b quando a for falso, sabendo que, de qualquer forma, o resultado será false , o que significa que você só precisa testar:

  1. a é verdadeiro e b é verdadeiro,
  2. a é falso,
  3. a é verdadeiro e b é falso.

Agora imagine o número de testes que você precisa para o seu one-liner. Isso explica a verbosidade.

All tests for this kind of methods I've read so far are very verbose, not complete and hard to understand.

  • eu expliquei a verbosidade.
  • Para completar, imagino que você esteja falando sobre o que ilustrei com três testes, em vez de quatro.
  • Finalmente, é óbvio que seus testes são difíceis de entender, já que o seu one-liner é muito complexo. Faça alguma refatoração e teste métodos separados, eventualmente usando a Injeção de dependência ou movendo esses métodos separados para classes dedicadas, se apropriado.
por 27.06.2014 / 11:57
fonte
0

Deixe-me apresentar outra perspectiva. Se o seu bloco de código real é tão complexo quanto o seu exemplo, não é uma boa prática escrevê-lo dessa maneira.

"Concise" does not always mean "elegant". Sometimes, it just means "hard to debug".

Imagine se essa condição complexa estivesse de alguma forma falhando - seria totalmente irritante descobrir o problema. Uma maneira melhor é ter variáveis intermediárias armazenando os valores dos blocos de condição individuais. Obviamente, essas variáveis intermediárias precisam de nomes próprios para ajudar na legibilidade.

Expanda esse bloco de código "breve" para a série if-else aninhada correspondente. Cada pequeno if e else nesse bloco expandido é uma condição de teste em potencial. Agora você verá que não é breve, afinal. É uma lógica complexa que deve ser tratada com o devido respeito.

Com relação à sua pergunta, seria útil separar cada uma dessas condições de teste em seu próprio teste. Eles podem compartilhar configurações comuns, etc. usando funções. No nível de teste de unidade, os testes individuais mais simples são melhores porque ajudam você a apontar o problema para a condição de falha exata.

Organize cada teste de acordo com o padrão 3A . Isso torna os testes legíveis e também independentes uns dos outros. Aqui está um exemplo:

void testSummation()
{
    // ARRANGE
    Calculator testObject = new Calculator();

    // ACT
    int sum = testObject.add(1, 2);

    // ASSERT
    Assert.equals(3, sum);
}


Organizar:
Configure o ambiente para a condição a ser testada. Isso pode incluir a configuração de alguns parâmetros de inicialização, simulações, etc.

Lei:
Execute uma ação no objeto. Uma boa prática é garantir que essa ação seja uma operação independente, conforme vista da API do objeto.

Assert:
Verifique se a ação teve o efeito pretendido. Valide as variáveis de saída, o estado do objeto, etc. para certificar-se de que o comportamento esperado foi atendido.

    
por 27.06.2014 / 11:57
fonte