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


58

Nei miei test unitari, spesso lancio 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 sono, ad esempio, condizioni al contorno, anche se faccio un test anche su quelli). Farei fatica a trovare buoni nomi per questi numeri e scrivere qualcosa del genere const int TWO = 2;è ovviamente inutile. È corretto scrivere i test in questo modo o dovrei fattorizzare i numeri in costanti?

In Tutti i numeri magici sono 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.


9
Se stai inserendo valori e ti aspetti di poter rileggere quegli stessi valori, direi che i numeri magici vanno bene. Quindi, se, per esempio, 1, 2, 3sono indici di array 3D in cui hai precedentemente memorizzato il valore 17, allora penso che questo test sarebbe dandy (purché tu abbia anche dei test negativi). Ma se è il risultato di un calcolo, dovresti assicurarti che chiunque legga questo test capirà perché foo(1, 2, 3)dovrebbe essere 17, e probabilmente i numeri magici non raggiungeranno questo obiettivo.
Joe White,

24
const int TWO = 2;è anche peggio del semplice utilizzo 2. È conforme alla formulazione della regola con l'intento di violare il suo spirito.
Agent_L

4
Che cos'è un numero che "non significa nulla"? Perché sarebbe nel tuo codice se non significasse nulla?
Tim Grant,

6
Sicuro. Lascia un commento prima di una serie di tali test, ad esempio "una piccola selezione di esempi determinati manualmente". Ciò, in relazione agli altri test che stanno chiaramente testando i confini e le eccezioni, sarà chiaro.
davidbak,

5
Il tuo esempio è fuorviante: quando il nome della tua funzione sarebbe davvero foo, non significherebbe nulla e quindi i parametri. Ma in realtà, sono abbastanza sicuro che la funzione non ha quel nome, ed i parametri non hanno un nome bar1, bar2e bar3. Fai un esempio più realistico in cui i nomi hanno un significato, quindi ha molto più senso discutere se anche i valori dei dati di test hanno bisogno di un nome.
Doc Brown,

Risposte:


80

Quando hai davvero dei numeri che non hanno alcun significato?

Di solito, quando i numeri hanno qualche significato, è necessario assegnarli alle variabili locali del metodo di test per rendere il codice più leggibile e autoesplicativo. I nomi delle variabili dovrebbero almeno riflettere il significato della 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 ha un nome HUNDRED_DOLLARS_ZERO_CENT, ma startBalanceper indicare qual è il significato della variabile ma non che il suo valore sia in alcun modo speciale.


3
@Kevin - in che lingua stai testando? Alcuni framework di test consentono di impostare fornitori di dati che restituiscono una serie di array di valori per i test
HorusKol,

10
Anche se sono d'accordo con l'idea, attenzione che questa pratica può introdurre anche nuovi errori, come se si estraesse accidentalmente un valore simile 0.05fa un int. :)
Jeff Bowman,

5
+1 - grandi cose. Solo perché non ti interessa quale sia un valore particolare, ciò non significa che non sia ancora un numero magico ...
Robbie Dee

2
@PieterB: AFAIK è colpa di C e C ++, che ha formalizzato la nozione di constvariabile.
Steve Jessop,

2
Le tue variabili sono state identiche ai parametri nominati di calculateCompoundInterest? In tal caso, la digitazione aggiuntiva è una prova di lavoro che hai letto la documentazione per la funzione che stai testando, o almeno copiato i nomi che ti sono stati dati dal tuo IDE. Non sono sicuro di quanto questo dica al lettore l'intenzione del codice, ma se si passano i parametri nell'ordine sbagliato almeno possono dire cosa era previsto.
Steve Jessop,

20

Se stai usando numeri arbitrari solo per vedere cosa fanno, allora quello che stai veramente cercando sono probabilmente dati di test generati casualmente o test basati su proprietà.

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

Pensa a un normale test unitario come qualcosa di simile al seguente:

  1. Imposta alcuni dati.
  2. Eseguire alcune operazioni sui dati.
  3. Asserire qualcosa sul risultato.

L'ipotesi consente di scrivere test che invece assomigliano a questo:

  1. Per tutti i dati che corrispondono ad alcune specifiche.
  2. Eseguire alcune operazioni sui dati.
  3. Asserire qualcosa sul risultato.

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

Il punto 3 può essere fonte di confusione per alcune persone, quindi chiariamo. Ciò non significa che stai affermando la risposta esatta - questo è ovviamente impossibile da fare per input arbitrari. Invece, affermi 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 auto-bilanciante è effettivamente bilanciato (usando qualunque criterio abbia quella particolare struttura di dati).

Nel complesso, scegliere da soli numeri arbitrari è probabilmente piuttosto negativo: non aggiunge davvero un sacco di valore e confonde chiunque lo legga. Generare automagicamente un mucchio di dati di test casuali e usarlo in modo efficace è buono. Trovare una libreria simile a Ipotesi o QuickCheck per la tua lingua preferita è probabilmente un modo migliore per raggiungere i tuoi obiettivi rimanendo comprensibile agli altri.


11
I test casuali possono trovare bug difficili da riprodurre ma i test casuali non trovano bug riproducibili. Assicurati di catturare eventuali errori di test con un caso di test riproducibile specifico.
JBR Wilkinson,

5
E come fai a sapere che il tuo test unitario non viene infastidito quando "affermi qualcosa sul risultato" (in questo caso, ricalcola ciò che foosta calcolando) ...? Se tu fossi sicuro al 100% che il tuo codice dia la risposta giusta, allora inseriresti quel codice nel programma e non lo testerei. In caso contrario, è necessario testare il test e penso che tutti vedano dove sta andando.

2
Sì, se passi input casuali in una funzione devi sapere quale sarebbe l'output per poter affermare che funziona correttamente. Con i valori di test fissi / scelti puoi ovviamente risolverlo manualmente, ecc. Ma sicuramente qualsiasi metodo automatizzato per determinare se il risultato è corretto è soggetto agli stessi identici problemi della funzione che stai testando. O usi l'implementazione che hai (che non puoi perché stai testando se funziona) o scrivi una nuova implementazione che è altrettanto probabile che sia buggy (o più altrimenti useresti più è probabile che sia corretta ).
Chris,

7
@NajibIdrissi - non necessariamente. Ad esempio, è possibile verificare che l'applicazione del risultato dell'operazione inversa al risultato restituisca il valore iniziale iniziato. Oppure potresti testare gli invarianti attesi (ad es. Per tutti i calcoli degli interessi a dgiorni, il calcolo a dgiorni + 1 mese dovrebbe essere un tasso percentuale mensile noto superiore), ecc.
Jules,

12
@Chris - In molti casi, verificare che i risultati siano corretti è più semplice che generarli. Anche se questo non è vero in tutte le circostanze, ci sono molti dove si trova. Esempio: l'aggiunta di una voce a un albero binario bilanciato dovrebbe comportare un nuovo albero che è anche bilanciato ... facile da testare, abbastanza difficile da implementare nella pratica.
Jules,

11

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

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

E infine, se i tuoi unittest sono abbastanza complicati da renderli difficili / non pratici, probabilmente hai funzioni troppo complicate e potresti considerare il perché.

Più sciatamente scrivi test, peggio sarà il tuo codice reale. Se senti la necessità di nominare i valori del tuo test per chiarire il test, ciò suggerisce fortemente che il tuo metodo reale necessita di una migliore denominazione e / o documentazione. Se trovi la necessità di nominare le costanti nei test, esaminerei perché ne hai bisogno - probabilmente il problema non è il test stesso ma l'implementazione


Questa risposta sembra riguardare la difficoltà di inferire lo scopo di un test, mentre la vera domanda riguarda i numeri magici nei parametri del metodo ...
Robbie Dee,

@RobbieDee il nome / la documentazione per un test dovrebbe fornire il contesto e la spiegazione appropriati di qualsiasi numero magico presente nel test. In caso contrario, aggiungere la documentazione o rinominare il test per essere più chiari.
Enderland,

Sarebbe comunque meglio dare nomi ai numeri magici. Se il numero di parametri dovesse cambiare, la documentazione rischia di diventare obsoleta.
Robbie Dee,

1
@RobbieDee tieni presente che la funzione stessa ha parametri che si spera abbiano nomi significativi. Copiarli nel tuo test per nominare gli argomenti è piuttosto inutile.
Enderland,

"Speriamo" eh? Perché non semplicemente codificare la cosa correttamente e eliminare ciò che è apparentemente un numero magico come Philipp ha già delineato ...
Robbie Dee,

9

Questo dipende fortemente dalla funzione che stai testando. Conosco molti casi in cui i singoli numeri non hanno un significato speciale per conto proprio, ma il caso di test nel suo insieme è costruito in modo ponderato e quindi ha un significato specifico. Questo è ciò che si dovrebbe documentare in qualche modo. Ad esempio, se foodavvero è un metodo testForTriangleche decide se i tre numeri potrebbero essere lunghezze valide dei bordi di un triangolo, i tuoi test potrebbero apparire così:

// 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. È possibile migliorare ciò e trasformare i commenti in un parametro di messaggio assertEqualche verrà visualizzato quando il test fallisce. È quindi possibile migliorare ulteriormente questo aspetto e trasformarlo in un test basato sui dati (se il framework di test lo supporta). Tuttavia ti fai un favore se inserisci una nota nel codice per cui hai scelto questi numeri e quale dei vari comportamenti stai testando con il singolo caso.

Naturalmente, per altre funzioni i singoli valori per i parametri potrebbero essere più importanti, quindi usare un nome di funzione insignificante come fooquando si chiede come gestire il significato dei parametri non è probabilmente la migliore idea.


Soluzione sensata.
user1725145

6

Perché vogliamo usare Costanti nominate anziché numeri?

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

Se si scrivono diversi test unitari, ciascuno con un assortimento di 3 numeri (startBalance, interesse, anni), impaccherei semplicemente i valori nel test unitario come variabili locali. L'ambito più piccolo a cui 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 parametri denominati, questo è ovviamente superfluo. Lì avrei semplicemente impacchettato i valori grezzi nella chiamata del metodo. Non riesco a immaginare alcun refactoring che renda questa affermazione più concisa:

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

Oppure utilizza un framework di test, che ti permetterà di definire i casi di test in un formato array o mappa:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }

3

... ma in questo caso i numeri in realtà non hanno alcun significato

I numeri vengono utilizzati per chiamare un metodo, quindi la premessa di cui sopra non è corretta. Potrebbe non interessarti quali siano i numeri, ma questo è accanto al punto. Sì, potresti dedurre a cosa servono i numeri da alcuni maghi IDE, ma sarebbe molto meglio se tu fornissi solo i nomi dei valori, anche se corrispondono solo ai parametri.


1
Questo non è necessariamente vero, però - come nell'esempio dell'ultimo test unit che ho scritto ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). In questo esempio, 42è solo un valore segnaposto che viene prodotto dal codice nello script di test denominato lvalue_operatorse quindi verificato quando viene restituito dallo script. Non ha alcun significato, a parte il fatto che lo stesso valore si presenta in due luoghi diversi. Quale sarebbe un nome appropriato qui che effettivamente dà qualche significato utile?
Jules,

3

Se vuoi testare una funzione pura su un set di input che non sono condizioni al contorno, allora quasi sicuramente vuoi testarla su un intero gruppo di input che non sono (e sono) condizioni al contorno. E per me ciò significa che dovrebbe esserci una tabella di valori con cui chiamare la funzione 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, baze blurfdovrebbero essere sostituiti da nomi significativi come discusso nella 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 mettere i tuoi numeri in un array, forse un array di record, probabilmente sono dati Al contrario, se sospetti di avere dei dati a portata di mano, considera di metterli in un array e acquisirne di più.)


1

I test sono diversi dal codice di produzione e, almeno nei test di unità scritti in Spock, che sono brevi e puntuali, non ho alcun problema a usare 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 più difficile da leggere. Se la logica è "Quando aggiungo un utente di nome Smith, vedo l'utente Smith restituito nell'elenco degli utenti", non ha senso estrarre "Smith" su una costante.

Questo ovviamente si applica se si possono facilmente abbinare i valori usati nel blocco "dato" (setup) a quelli trovati nei blocchi "quando" e "allora". Se la configurazione del test è separata (nel codice) dal luogo in cui vengono utilizzati i dati, potrebbe essere meglio utilizzare costanti. Ma poiché i test sono i migliori autonomi, l'installazione è solitamente vicina al luogo di utilizzo e si applica il primo caso, il che significa che le costanti magiche sono abbastanza accettabili in questo caso.


1

In primo luogo, concordiamo sul fatto che "unit test" è spesso usato per coprire tutti i test automatici scritti da un programmatore e che è inutile discutere di ciò che ogni test dovrebbe essere chiamato ...

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

Lo ha fatto usando molti numeri casuali per ottenere un punto di partenza, quindi usando un "climber" per migliorare il risultato. Questo è stato eseguito molte volte, selezionando il miglior risultato. È possibile eseguire il seeding di un generatore di numeri casuali, in modo che fornisca sempre gli stessi numeri nello stesso ordine, quindi se il test imposta un seed, sappiamo che il risultato sarebbe lo stesso ad ogni serie.

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 cosa ha fatto quella parte del sistema.

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

Mentre "prendevamo in giro" il database, potevi chiamare questi test "unit test", ma l '"unità" era piuttosto grande.

Spesso quando si lavora su un sistema senza test, si esegue qualcosa di simile a quanto sopra, in modo da poter confermare il refactoring non modificare l'output; speriamo che vengano scritti test migliori per il nuovo codice!


1

Penso che in questo caso i numeri debbano essere definiti 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 "handle" unici (che dovrebbero essere sostituiti con costanti nominate, ovviamente), ma possono anche essere costanti precalcolate come "la velocità di un passero europeo a vuoto in furlong per due settimane", dove viene inserito il valore numerico senza commenti o contesto utile.


0

Non mi avventurerò per dire un sì / no definitivo, ma ecco alcune domande che dovresti porti quando decidi se va bene o no.

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

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

  3. Scrivendo il numero 2, come TWOpotrebbe 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 invece il test è assertEquals(numCustomersInBank(BANK_1), TWO), allora questo non fa che molto senso. Perché non BANK_1contenere due clienti? Per cosa stiamo testando?
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.