I numeri magici sono accettabili nei test unitari se i numeri non significano nulla?

58

Nei miei test unitari, lancio spesso valori arbitrari sul mio codice per vedere cosa fa. Ad esempio, se so che foo(1, 2, 3) dovrebbe restituire 17, potrei scrivere questo:

assertEqual(foo(1, 2, 3), 17)

Questi numeri sono puramente arbitrari e non hanno un significato più ampio (non lo sono, ad esempio, le condizioni al contorno, anche se lo faccio anche su quelli). Farei fatica a trovare un buon nome per questi numeri, e scrivere qualcosa come const int TWO = 2; è ovviamente inutile. È corretto scrivere i test in questo modo, oppure dovrei calcolare i numeri in costanti?

In Tutti i numeri magici sono stati creati uguali? , abbiamo appreso che i numeri magici sono OK se il significato è ovvio dal contesto, ma in questo caso i numeri in realtà non hanno alcun significato.

    
posta Kevin 20.06.2016 - 01:01
fonte

11 risposte

81

Quando hai davvero numeri che non hanno alcun significato?

Di solito, quando i numeri hanno un significato, dovresti assegnarli alle variabili locali del metodo di test per rendere il codice più leggibile e auto-esplicativo. I nomi delle variabili dovrebbero almeno riflettere ciò che significa la variabile, non necessariamente il suo valore.

Esempio:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

Si noti che la prima variabile non è denominata HUNDRED_DOLLARS_ZERO_CENT , ma startBalance per indicare qual è il significato della variabile ma non che il suo valore sia in alcun modo speciale.

    
risposta data 20.06.2016 - 01:04
fonte
20

Se stai usando numeri arbitrari solo per vedere cosa fanno, allora quello che stai davvero cercando è probabilmente dati test casuali o test basati su proprietà.

Ad esempio, Hypothesis è una fantastica libreria Python per questo tipo di test, ed è basata su QuickCheck .

Think of a normal unit test as being something like the following:

  1. Set up some data.
  2. Perform some operations on the data.
  3. Assert something about the result.

Hypothesis lets you write tests which instead look like this:

  1. For all data matching some specification.
  2. Perform some operations on the data.
  3. Assert something about the result.

L'idea è di non limitarti ai tuoi valori, ma scegli quelli casuali che possono essere usati per verificare che le tue funzioni corrispondano alle loro specifiche. Come nota importante, questi sistemi in genere ricordano qualsiasi input che non riesce e quindi assicurano che tali input vengano sempre testati in futuro.

Il punto 3 può essere fonte di confusione per alcune persone, quindi chiariamolo. Ciò non significa che stai affermando la risposta esatta - questo è ovviamente impossibile da fare per un input arbitrario. Invece, asserisci qualcosa su una proprietà del risultato. Ad esempio, potresti affermare che dopo aver aggiunto qualcosa a un elenco diventa non vuoto, o che un albero di ricerca binaria autoequilibrante è effettivamente bilanciato (usando qualunque criterio abbia quella particolare struttura di dati).

Complessivamente, il prelievo di numeri arbitrari è probabilmente piuttosto brutto - in realtà non aggiunge un sacco di valore e confonde chi lo legge. Generare automaticamente una serie di dati di test casuali e usarli efficacemente è buono. Trovare un'ipotesi o una libreria simile a QuickCheck per il linguaggio che hai scelto è probabilmente un modo migliore per raggiungere i tuoi obiettivi rimanendo comprensibile agli altri.

    
risposta data 20.06.2016 - 04:56
fonte
11

Il nome del test dell'unità dovrebbe fornire la maggior parte del contesto. Non dai valori delle costanti. Il nome / documentazione per un test dovrebbe fornire il contesto appropriato e la spiegazione di qualsiasi numero magico presente nel test.

Se ciò non è sufficiente, un po 'di documentazione dovrebbe essere in grado di fornirlo (tramite il nome della variabile o una docstring). Tieni presente che la funzione stessa ha parametri che, si spera, hanno nomi significativi. Copiare quelli nel tuo test per nominare gli argomenti è piuttosto inutile.

E infine, se le tue richieste sono abbastanza complicate da rendere difficile / non pratico, probabilmente hai funzioni troppo complicate e potresti considerare il motivo per cui questo è il caso.

Più scrupolosamente scrivi i test, peggiore sarà il tuo codice reale. Se senti la necessità di nominare i valori del test per rendere chiaro il test, suggerisce strongmente che il tuo metodo effettivo ha bisogno di un nome e / o documentazione migliori. Se trovi la necessità di nominare le costanti nei test, cercherò perché ne hai bisogno - probabilmente il problema non è il test stesso, ma l'implementazione

    
risposta data 20.06.2016 - 04:39
fonte
9

Dipende molto dalla funzione che stai testando. Conosco molti casi in cui i singoli numeri non hanno un significato speciale da soli, ma il test case nel suo complesso è costruito in modo ponderato e quindi ha un significato specifico. Questo è ciò che si dovrebbe documentare in qualche modo. Ad esempio, se foo è davvero un metodo testForTriangle che decide se i tre numeri potrebbero essere lunghezze valide dei bordi di un triangolo, i tuoi test potrebbero essere simili a questo:

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

e così via. Potresti migliorare questo e trasformare i commenti in un parametro di messaggio di assertEqual che verrà visualizzato quando il test fallisce. È quindi possibile migliorare ulteriormente questo e refactoring questo in un test guidato dai dati (se il vostro framework di test supporta questo). Ciononostante ti fai un favore se inserisci una nota nel codice perché hai scelto questi numeri e quali dei vari comportamenti che stai testando con il singolo caso.

Naturalmente, per altre funzioni i singoli valori dei parametri potrebbero essere più importanti, quindi usare un nome di funzione senza senso come foo quando si chiede come gestire il significato dei parametri probabilmente non è l'idea migliore.

    
risposta data 20.06.2016 - 22:32
fonte
6

Perché vogliamo utilizzare costanti denominate anziché numeri?

  1. ASCIUTO - Se ho bisogno del valore a 3 posizioni, voglio solo definirlo una volta, quindi posso cambiarlo in un posto, se cambia.
  2. Dai il significato ai numeri.

Se scrivi diversi test unitari, ognuno con un assortimento di 3 numeri (startBalance, interessi, anni), impaccherei i valori nel test unitario come variabili locali. L'ambito più piccolo al quale appartengono.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

Se si utilizza un linguaggio che consente i parametri denominati, questo è ovviamente superfluo. Lì vorrei solo impacchettare i valori grezzi nella chiamata al metodo. Non riesco a immaginare alcun refactoring rendendo questa affermazione più concisa:

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

O usa un framework di test, che ti permetterà di definire i casi di test in alcuni array o in formato Map:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }
    
risposta data 20.06.2016 - 14:35
fonte
3

...but in this case the numbers actually have no meaning at all

I numeri vengono usati per chiamare un metodo, quindi sicuramente la premessa sopra è sbagliata. Non puoi curare quali sono i numeri ma questo è accanto al punto. Sì, puoi dedurre per che cosa i numeri sono usati da alcune magie IDE, ma sarebbe molto meglio se tu dessi i nomi dei valori - anche se corrispondono semplicemente ai parametri.

    
risposta data 20.06.2016 - 10:05
fonte
3

Se vuoi testare una funzione pura su un insieme di input che non sono condizioni al contorno, allora quasi certamente voglio testarlo su un gruppo intero di insiemi di input che non sono (e sono) condizioni al contorno. E per me questo significa che ci dovrebbe essere una tabella di valori per chiamare la funzione con, e un ciclo:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Strumenti come quelli suggeriti nella risposta di Dannnno possono aiutarti a costruire la tabella dei valori da testare. bar , baz e blurf devono essere sostituiti da nomi significativi come discusso in risposta di Philipp .

(Principio generale discutibile qui: i numeri non sono sempre "numeri magici" che hanno bisogno di nomi, invece i numeri potrebbero essere dati . Se avesse senso inserire i numeri in un array, forse un array di record, quindi probabilmente sono dati. Al contrario, se si sospetta che si possano avere dati a portata di mano, si consideri di inserirli in un array e acquisirne di più.)

    
risposta data 20.06.2016 - 21:53
fonte
1

Innanzitutto, siamo d'accordo sul fatto che "unit test" è spesso usato per coprire tutti i test automatici scritti da un programmatore, e che è inutile discutere su quale dovrebbe essere chiamato ogni test ....

Ho lavorato su un sistema in cui il software richiedeva molti input e ha elaborato una "soluzione" che doveva soddisfare alcuni vincoli, ottimizzando al contempo altri numeri. Non c'erano risposte giuste, quindi il software doveva solo dare una risposta ragionevole.

Lo ha fatto usando un sacco di numeri casuali per ottenere un punto di partenza, quindi usando un "alpinista" per migliorare il risultato. Questo è stato eseguito molte volte, scegliendo il miglior risultato. Un generatore di numeri casuali può essere seminato, in modo che fornisca sempre gli stessi numeri nello stesso ordine, quindi se il test ha impostato un seme, sappiamo che il risultato sarebbe lo stesso in ogni esecuzione.

Abbiamo fatto molti test che hanno fatto quanto sopra e verificato che i risultati fossero gli stessi, questo ci ha detto che non avevamo cambiato ciò che quella parte del sistema ha fatto per errore durante il refactoring ecc. Non ci ha detto nulla sulla correttezza di ciò che faceva quella parte del sistema.

Questi test erano costosi da mantenere, poiché ogni modifica al codice di ottimizzazione avrebbe interrotto i test, ma hanno anche trovato alcuni bug nel codice molto più grande che pre-elaborava i dati e post-elaborava i risultati.

Dato che abbiamo "deriso" il database, potremmo chiamare questi test "unit test", ma "l'unità" era piuttosto grande.

Spesso quando lavori su un sistema senza test, fai qualcosa di simile a quanto sopra, in modo che tu possa confermare che il tuo refactoring non cambi l'output; speriamo che migliori test vengano scritti per il nuovo codice!

    
risposta data 20.06.2016 - 13:19
fonte
1

Penso che in questo caso i numeri dovrebbero essere chiamati numeri arbitrari, piuttosto che numeri magici, e commentare la riga come "caso di test arbitrario".

Certo, alcuni numeri magici possono anche essere arbitrari, come per i valori unici di "maniglia" (che dovrebbero essere sostituiti con le costanti nominate, ovviamente), ma possono anche essere costanti precalcolate come "velocità di un passero europeo a vuoto in quindicina ", in cui il valore numerico è inserito senza commenti o contesto utile.

    
risposta data 22.06.2016 - 17:11
fonte
0

I test sono diversi dal codice di produzione e, almeno nei test di unità scritti in Spock, che sono brevi e al punto, non ho problemi con le costanti magiche.

Se un test è lungo 5 righe e segue lo schema di base dato / quando / allora, l'estrazione di tali valori in costanti renderebbe il codice più lungo e difficile da leggere. Se la logica è "Quando aggiungo un utente di nome Smith, vedo l'utente che Smith ha restituito nell'elenco degli utenti", non vi è alcun motivo per estrarre "Smith" su una costante.

Questo ovviamente si applica se puoi facilmente far corrispondere i valori usati nel blocco "dato" (setup) a quelli trovati nei blocchi "when" e "then". Se la configurazione del test è separata (in codice) dal luogo in cui vengono utilizzati i dati, potrebbe essere meglio usare le costanti. Ma dal momento che i test sono meglio autosufficienti, l'installazione di solito è vicina al luogo di utilizzo e si applica il primo caso, il che significa che le costanti magiche sono abbastanza accettabili in questo caso.

    
risposta data 20.06.2016 - 13:24
fonte
0

Non mi avventuro fino al punto di dire un sì / no definitivo, ma qui ci sono alcune domande che dovresti porci quando decidi se è OK o meno.

  1. Se i numeri non significano nulla, perché sono lì in primo luogo? Possono essere sostituiti da qualcos'altro? Puoi eseguire la verifica in base alle chiamate e al flusso di metodi anziché alle asserzioni di valore? Prendi in considerazione qualcosa come il metodo verify() di Mockito che controlla se certe chiamate al metodo sono state fatte o meno per falsificare oggetti invece di asserire effettivamente un valore.

  2. Se i numeri fanno significano qualcosa, allora dovrebbero essere assegnati a variabili che sono nominate in modo appropriato.

  3. Scrivere il numero 2 come TWO potrebbe essere utile in certi contesti, e non tanto in altri contesti.

    • Ad esempio: assertEquals(TWO, half_of(FOUR)) ha senso per qualcuno che legge il codice. È immediatamente chiaro cosa stai testando.
    • Se tuttavia il tuo test è assertEquals(numCustomersInBank(BANK_1), TWO) , questo non rende quello molto sensato. Perché BANK_1 contiene due clienti? Cosa stiamo testando?
risposta data 21.06.2016 - 14:02
fonte

Leggi altre domande sui tag