Come dovrei testare la funzione booleana con molte possibili permutazioni

4

Quando scrivo un test unitario di solito fornisco un contesto (oggetto semplice o oggetto deriso / stub) che imposto in qualche modo e quindi posso eseguire la dichiarazione di assert sul contesto:

nota: il codice è in pseudo-codice; groovy like syntax:

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

Ma come organizzare il test quando è necessario testare un'espressione booleana complessa che richiede molti parametri?

EDIT: ho sostituito l'espressione a una riga con qualcosa di più leggibile per evitare confusione.

//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) 
  }
}

Tutti i test per questo tipo di metodi che ho letto finora sono molto prolissi, non completi e difficili da capire.

Ed è un peccato che una linea così piccola di codice, anche se è "complessa", genera test terribili.

Ho provato a suddividere l'espressione booleana in più piccoli metodi secondari, ma a volte non è così conveniente e il conteggio delle permutazioni è ancora alto. Di solito, inoltre, spezzo l'espressione in variabili intermedie, ma questo non aiuta il mondo dei test unitari ...

Come dovrei provare qualcosa del genere per fare in modo che il codice di test corrisponda alla brevità del codice testato e alla completezza che deve affermare che il mio codice funziona come previsto?

Modifica: sulla soluzione mantenuta.

Il refactoring è davvero una strada da percorrere, ma in realtà non voglio "srotolare" tutte le combinazioni manualmente nei miei test: è prolissa, brutta e difficile da capire.

Tuttavia terrò questa risposta come passaggio obbligatorio preliminare: rompere l'espressione in parti più piccole prima di ogni altra cosa.

Una volta eseguito il refactoring e il test continua a fare test combinatori, utilizzerò la soluzione di TruthTable proposta. Creerò solo le combinazioni e non dichiarerò tutto.

Ho trovato questo interessante, ma obsoleto, articolo sulla combinazione di test in Groovy.
Lo strumento di cui hanno parlato è fuori dal radar del motore di ricerca, quindi deve essere morto!
Comunque userò lo stesso schema:

assertThat(permutations, expectations, instanceObject)

Dove

    La permutazione
  • è tutti i valori possibili da assegnare alle proprietà in una "mappa condensata": (a: [vero, falso], b: [vero, falso], ...)
  • l'aspettativa è tutte le combinazioni che restituiscono un valore specifico, tutte le altre combinazioni verranno verificate rispetto a un valore predefinito.
posta Guillaume 27.06.2014 - 11:36
fonte

3 risposte

11

Un modo abbastanza intuitivo per gestirlo è codificare una tabella di verità nel test in modo da avere qualcosa di simile:

//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...
}

vale a dire. un test guidato dai dati. Per il grande tavolo è possibile spostarlo in un file di dati che si snellisce. Il test stesso quindi ha semplicemente bisogno di scorrere sulla tabella e collegare gli ingressi e controllare l'output previsto.

Questo suggerisce anche un possibile refactoring del codice. Un input di Tuple<bool,bool> è essenzialmente uguale a un enum con 4 possibili valori.

    
risposta data 27.06.2014 - 12:08
fonte
10

Il tuo errore è dare per scontato che il codice sia breve. Il fatto che sia one-liner non significa che sia semplice testare, eseguire il debug e mantenere. Se dovessi mantenere questo codice, avrei WTFed parecchio su persone a cui piace scrivere codice criptico e condensato (anche, io spero le tue a , b ecc. Le variabili sono solo per un esempio e nella vita reale usi nomi più significativi)

Ecco come appare quando refactored, isSomething , isAlso e isSomethingElse devono essere sostituiti da nomi significativi che document il tuo codice:

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)
}

Questo codice è molto più semplice di quello singolo, ma rende anche più chiaro che avrai bisogno di più di due-tre test unitari per testarlo.

Numero di permutazioni

Parlando del numero elevato di permutazioni che ti aspetteresti:

return a

richiede due test : uno in cui l'istruzione restituisce true e uno in cui restituisce false.

return a && b

ha bisogno di tre o quattro test , a seconda della lingua. La maggior parte delle lingue sarebbe pigra ed eviterebbe di valutare b quando a è falso, sapendo che comunque il risultato sarà false , il che significa che devi solo testare:

  1. a è vero e b è vero,
  2. a è false,
  3. a è vero e b è falso.

Ora immagina il numero di test necessari per il tuo one-liner. Questo spiega la verbosità.

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

  • Ho spiegato la verbosità.
  • Per quanto completo, immagino tu stia parlando di ciò che ho illustrato con tre test anziché quattro.
  • Infine, è ovvio che i tuoi test sono difficili da capire, dal momento che il tuo one-liner è troppo complesso. Esegui il refactoring e testane i metodi separati, eventualmente utilizzando Dependency Injection o spostando questi metodi separati in classi dedicate, se necessario.
risposta data 27.06.2014 - 11:57
fonte
0

Permettetemi di presentare un'altra prospettiva. Se il tuo vero blocco di codice è complesso come il tuo esempio, non è una buona pratica scriverlo in questo modo.

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

Immagina se quella condizione complessa fosse in qualche modo fallita - sarebbe decisamente fastidioso scoprire il problema. Un modo migliore è quello di avere variabili intermedie che memorizzano i valori dei singoli blocchi di condizioni. Naturalmente, quelle variabili intermedie hanno bisogno di nomi propri per facilitare la leggibilità.

Espandi il blocco di codice "breve" nella serie ifid else annidata corrispondente. Ogni piccolo se e altro in quel blocco espanso è una potenziale condizione di test. Ora vedrai che non è breve, dopo tutto. È una logica complessa che deve essere trattata con il dovuto rispetto.

Per quanto riguarda la tua domanda, sarebbe utile separare ciascuna di queste condizioni di test nel proprio test. Possono condividere impostazioni comuni, ecc. Usando le funzioni. A livello di test unitario, i singoli test più semplici sono migliori perché aiutano a localizzare il problema esattamente alla condizione di errore.

Organizza ciascun test come descritto nel pattern 3A . Ciò rende i test leggibili e indipendenti l'uno dall'altro. Ecco un esempio:

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

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

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


Disponi:
Impostare l'ambiente per la condizione da testare. Ciò potrebbe includere l'impostazione di alcuni parametri di inizializzazione, mock, ecc.

Act:
Esegui un'azione sull'oggetto. Una buona pratica è assicurarsi che questa azione sia un'operazione indipendente vista dall'API dell'oggetto.

asserzione:
Controlla se l'azione ha avuto l'effetto desiderato. Convalidare le variabili di output, lo stato dell'oggetto, ecc. Per accertarsi che il comportamento previsto sia stato rispettato.

    
risposta data 27.06.2014 - 11:57
fonte

Leggi altre domande sui tag