Come devo testare le formule matematiche unitarie? [duplicare]

9

Ho una serie di funzioni molto simili alla definizione matematica di una funzione . Ad esempio, una versione semplificata di una di queste funzioni può essere simile a:

int function foo(int a, int b) {
    return a * b;
}

Mi piacerebbe testarli unitamente, ma non sono sicuro di quale sia il modo migliore per avvicinarsi a questi test, poiché queste funzioni hanno intervalli molto ampi per i valori dei parametri. Ogni approccio che ho trovato mi sembra carente:

  • Se provo con un sottoinsieme discreto e predefinito di possibili valori, temo che il set che scelgo possa non essere rappresentativo del tutto, portando a un ciclo potenzialmente infinito di aggiungere nuovi valori al sottoinsieme ogni volta che un valore imprevisto è trovato. Questo sembra come sconfiggere il punto di avere un test unitario in primo luogo.
  • Se provo con un sottoinsieme discreto di valori possibili determinati casualmente, eredito tutti i problemi relativi alla selezione manuale del sottoinsieme oltre alla reale possibilità che i test falliscano casualmente ogni volta.
  • Se provo con tutti i valori possibili (ad es. da 0 a 2 64 ), i test richiederanno per sempre.

Il mio pensiero finale su come affrontare il problema è stato quello di evitare di testare l'implementazione della funzione interamente, o tramite:

  • Verifica solo che la funzione accetta due interi e restituisce un intero o
  • Non testare affatto la funzione

Ma se non sto testando le funzioni - in pratica o in linea di principio - non sono sicuro di come possa garantire la correttezza delle funzioni nel caso di un eventuale refactoring.

Qual è il modo migliore per testare funzioni che hanno un output completamente deterministico, ma possono operare su una vasta gamma di possibili input?

    
posta Evan 11.02.2015 - 23:42
fonte

5 risposte

7

È molto importante avere test riproducibili falliti o successivi, quindi dimentica qualsiasi approccio di input casuale. Diciamo che testate la funzione sin (x) e sapete che il suo risultato è sempre nell'intervallo [-1,1] e voi affermate di testare 100 numeri casuali, quindi la vostra asserzione è matematicamente corretta, ma cosa succede se c'è un peccato veloce sporco (x) implementazione che può produrre un 1.00000000000001 e il test fallisce quasi ogni 1000esima esecuzione.

Prova a trovare i parametri di input validi:

    I valori
  • producono buoni risultati arrotondati
  • punti di flesso
  • confini relativi e assoluti

Talvolta le funzioni parametrizzate mantengono quelle proprietà quando cambiano solo un parametro specifico (ad esempio gli zero crossing e un parametro di ridimensionamento), quindi prova anche alcuni valori dispari.

Riduci il codice boilerplate con i provider di dati dell'unità di test, se disponibili.

    
risposta data 12.02.2015 - 01:14
fonte
14

Crea uno o due casi di test tipici e poi concentra il test rimanente su condizioni al contorno

Ad esempio, come si comporta la tua funzione con gli zeri? con quelli? con int.MaxValue? Quali sono i più grandi numeri positivi o negativi che possono essere utilizzati nella funzione prima che trabocchi o underflow? Cosa succede quando si verifica un overflow o un underflow?

E così via.

    
risposta data 12.02.2015 - 00:25
fonte
8

Le funzioni matematiche sono particolarmente sensibili a test basati su proprietà .

Con i test basati sulle proprietà, definisci in modo dichiarativo invarianti di alto livello che la tua API deve soddisfare. Il framework di test genera un numero elevato di casi di test e tenta di trovare un controesempio per i tuoi invarianti.

Scriviamo alcune proprietà per il tuo esempio: una funzione * che moltiplica due numeri. I test basati sulle proprietà hanno avuto origine nella comunità di programmazione funzionale, quindi userò QuickCheck (uno strumento basato su Haskell) , ma quadri di test simili sono ora disponibili per la maggior parte delle lingue imperative.

Definiamo proprietà scrivendo funzioni che accettano valori arbitrari come argomenti. Per ciascuna di queste funzioni, QuickCheck genererà casualmente 100 combinazioni di argomenti e verificherà che la proprietà sia valida per ciascuno di essi.

prop_commutative x y = (x * y) == (y * x)

prop_associative x y z = (x * y) * z == x * (y * z)

prop_distributive x y z = x * (y + z) == (x * y) + (x * z)

prop_identity x = (1 * x) == x

prop_zero x = (0 * x) == 0

I nostri test sembrano definizioni matematiche! Dicono "Per tutti x e y , x * y equivale a y * x " e così via. Se, ad esempio, stavi sviluppando un set di funzioni trigonometriche, potresti scrivere un insieme simile di proprietà testando cose come le formule a doppio angolo . I test basati su proprietà sono davvero utili per la programmazione matematica.

L'output dell'esecuzione di questi test è simile a questo:

=== prop_commutative ===
+++ OK, passed 100 tests.

=== prop_associative ===
+++ OK, passed 100 tests.

=== prop_distributive ===
+++ OK, passed 100 tests.

=== prop_identity ===
+++ OK, passed 100 tests.

=== prop_zero ===
+++ OK, passed 100 tests.

Quando una proprietà non è vera, QuickCheck trova un contro-esempio. Questa proprietà non è valida per i numeri inferiori a 1:

prop_multiplyingMakesItBigger x y = (x * y) > x

QuickCheck individua il nostro errore e stampa i valori di x e y che hanno smentito la nostra proprietà:

=== prop_multiplyingMakesItBigger ===
*** Failed! Falsifiable (after 1 test):  
0
0

QuickCheck testa le tue proprietà con valori che non avevi previsto, il che ti aiuta a trovare i bug.

È anche possibile utilizzare test basati su proprietà per testare un modello. Se hai un'implementazione ingenua di una funzione che sai essere corretta (ma forse non è abbastanza veloce), puoi usarla come modello per il test di regressione di una versione migliorata. Ecco un esempio in cui controlliamo che * sia equivalente all'aggiunta ripetuta:

prop_repeatedAddingModel (NonNegative (Small x)) y = (x * y) == (foldr (+) 0 (replicate x y))

Qui sto anche dimostrando come usare QuickCheck per limitare i valori di test. Il nostro modello non funziona con x negativo, ed è proibitivamente lento per il grande x , quindi diciamo a QuickCheck che vogliamo x essere piccoli e non negativi.

Real World Haskell ha un grande capitolo su QuickCheck, con molti esempi.

    
risposta data 19.02.2015 - 22:31
fonte
1

Hai menzionato il dilemma di testare un set fisso di casi (che potrebbe semplicemente capitare di "perdere" i valori su cui la funzione non funziona) o testare molti casi generati casualmente (che possono passare una volta e fallire il successivo, ed è lento per l'avvio).

Entrambe di queste tecniche sono utili e non si escludono a vicenda. Può essere utile avere una suite di test (veloce) che usa un set fisso di casi, e un'altra (lenta) suite che genera molti, molti casi casuali e 1) controlla se le uscite obbediscono a determinati invarianti, così come 2) vedere se riscontri un'asserzione fallita o lanci un'eccezione non catturata.

Esegui la suite veloce su ogni commit. Esegui la suite lenta (randomizzata, generativa) tutte le volte che vuoi, forse prima di ogni uscita. Se la suite lenta rileva un input che "spezza" la tua funzione, aggiungi quell'input come un nuovo test case nella suite veloce.

Quando inizia la suite randomizzata, dovrebbe generare un nuovo seme casuale e stamparlo sulla console . Dovrebbe anche fornire un modo per impostare manualmente il seme, per i casi in cui si desidera ripetere esattamente una corsa precedente.

    
risposta data 12.02.2015 - 19:44
fonte
1

Perfect è sicuramente il nemico del bene quando si tratta di test. I test sono raramente facili. Devi veramente capire che cosa dovrebbe fare la tua funzione / formula. Anche qualcosa di semplice come moltiplicare due numeri ha diverse proprietà che possiamo controllare.

Potresti scoprire che alcuni di essi verranno visualizzati solo se non prendi in considerazione alcuni casi limite.

// Falso presupposto che quando i numeri vengono moltiplicati, il risultato è maggiore
pippo (a, b) > a + b
* non riesce su 2,2
// Aggiorna ipotesi errata
foo (a, b) > = a + b

// Un tempo negativo un negativo è un possibile isneg (a) e isneg (b) e ispos (foo (a, b))

// Test per zero
foo (a, 0) = 0
// Prova per 1
foo (a, 1) = a
// Proprietà commutativa
foo (a, b) = pippo (b, a)

// Proprietà associativa
pippo (a, b) * c = a * pippo (b, c)

Essere in grado di farlo significa non dover testare ogni singola combinazione di parametri.

Questo tipo di cose vengono catturate manualmente nel mondo degli affari tutto il tempo. Chiunque abbia sufficiente conoscenza del dominio può essere d'aiuto nel formulare queste ipotesi. Forse non conosco le cifre del rapporto trimestrale, ma è una scommessa sicura che non supereranno mai le cifre dell'anno precedente.

    
risposta data 12.02.2015 - 18:36
fonte

Leggi altre domande sui tag