Come scrivere "buoni" test unitari?


61

Innescato da questo thread , sto (di nuovo) pensando di utilizzare finalmente i test unitari nei miei progetti. Alcuni poster dicono qualcosa del tipo "I test sono belli, se sono buoni test". La mia domanda ora: quali sono i "buoni" test?

Nelle mie applicazioni, la parte principale è spesso una sorta di analisi numerica, a seconda di grandi quantità di dati osservati, e risulta in una funzione di adattamento che può essere utilizzata per modellare questi dati. Ho trovato particolarmente difficile costruire test per questi metodi, poiché il numero di possibili input e risultati è troppo grande per testare ogni singolo caso, e i metodi stessi sono spesso piuttosto lunghi e non possono essere facilmente rifattorizzati senza sacrificare le prestazioni. Sono particolarmente interessato a test "buoni" per questo tipo di metodo.


8
Qualsiasi buon test unitario dovrebbe testare solo una cosa: se fallisce, dovresti sapere esattamente cosa è andato storto.
gablin

2
Quando si dispone di grandi quantità di dati, la cosa buona è scrivere test generici che possano accettare file di dati come input. I file di dati dovrebbero in genere contenere sia input che risultati attesi. Con i framework di test xunit è possibile generare casi di test al volo, uno per ciascun campione di dati.
Froderik,

2
@gablin "Se fallisce dovresti sapere esattamente cosa è andato storto" suggerirebbe che i test con molteplici possibili cause di errore vanno bene, purché tu possa determinare la causa dall'output del test ...
user253751

Nessuno sembra aver menzionato che i test unitari possono testare quanto tempo richiede l'operazione. Puoi riformattare il tuo codice tenendo conto delle prestazioni, assicurandoti che il test unitario ti dica se passa o fallisce in base al tempo e ai risultati.
CJ Dennis,

Risposte:


52

The Art of Unit Testing ha il seguente da dire sui test unitari:

Un test unitario dovrebbe avere le seguenti proprietà:

  • Dovrebbe essere automatizzato e ripetibile.
  • Dovrebbe essere facile da implementare.
  • Una volta scritto, dovrebbe rimanere per uso futuro.
  • Chiunque dovrebbe essere in grado di eseguirlo.
  • Dovrebbe funzionare con la semplice pressione di un pulsante.
  • Dovrebbe funzionare rapidamente.

e poi aggiunge che dovrebbe essere completamente automatizzato, affidabile, leggibile e mantenibile.

Consiglio vivamente di leggere questo libro se non l'hai già fatto.

A mio avviso, tutti questi sono molto importanti, ma gli ultimi tre (affidabili, leggibili e gestibili) in particolare, come se i tuoi test abbiano queste tre proprietà, di solito anche il tuo codice ne ha.


1
+1 per un elenco completo destinato ai test unitari (non ai test di integrazione o funzionali)
Gary Rowe,

1
+1 per il collegamento. Materiale interessante da trovare lì.
Joris Meys,

1
"Esegui rapidamente" ha grandi implicazioni. È uno dei motivi per cui i test unitari devono essere eseguiti separatamente, lontano da risorse esterne come database, file system, servizio web, ecc. Ciò, a sua volta, porta a beffe / stub.
Michael Easter,

1
quando dice It should run at the push of a button, ciò significa che un test di unità non dovrebbe richiedere l'esecuzione di contenitori (server app) (per l'unità da testare) o una connessione di risorse (come DB, servizi Web esterni, ecc.)? Sono solo confuso su quali parti di un'applicazione debbano essere testate e quali no. Mi è stato detto che i test unitari non avrebbero dovuto dipendere dalla connessione DB e dai contenitori in esecuzione e forse avrebbero usato i modelli.
anfibio,

42

Un buon test unitario non rispecchia la funzione che sta testando.

Come esempio molto semplificato, considera di avere una funzione che restituisce una media di due int. Il test più completo chiamerebbe la funzione e verificherebbe se un risultato è in realtà una media. Questo non ha alcun senso: stai rispecchiando (replicando) la funzionalità che stai testando. Se hai commesso un errore nella funzione principale, commetterai lo stesso errore nel test.

In altre parole, se ti ritrovi a replicare la funzionalità principale nel test unitario, è probabile che stai sprecando il tuo tempo.


21
+1 Quello che faresti in questo caso è testare con argomenti hardcoded e verificare con la tua risposta nota.
Michael K,

Ho già visto quell'odore prima.
Paul Butcher,

Potresti dare un esempio di un buon test unitario per la funzione che restituisce le medie?
VLAS,

2
@VLAS verifica i valori predefiniti, ad es. Assicurati che avg (1, 3) == 2, controlla in modo ancora più importante i casi limite, come INT_MAX, zero, valori negativi, ecc. Se un bug è stato trovato e corretto nella funzione, aggiungine un altro test per assicurarsi che questo bug non venga mai reintrodotto.
Mojuba,

Interessante. Come proponete di ottenere le risposte corrette a tali input di test e di non commettere potenzialmente lo stesso errore del codice sottoposto al test?
Timo,

10

Buoni test unitari sono essenzialmente le specifiche in forma eseguibile:

  1. descrivere il comportamento del codice corrispondente ai casi d'uso
  2. coprire casi tecnici d'angolo (cosa succede se viene superato null) - se un test non è presente per un caso d'angolo, il comportamento non è definito.
  3. si interrompe se il codice testato cambia rispetto alle specifiche

Ho riscontrato che Test-Driven-Development è molto adatto per le routine di libreria poiché essenzialmente si scrive prima l'API e quindi l'implementazione effettiva.


7

per TDD, i "buoni" test mettono alla prova le funzionalità che il cliente desidera ; le caratteristiche non corrispondono necessariamente alle funzioni e gli scenari di test non devono essere creati dallo sviluppatore nel vuoto

nel tuo caso - suppongo - la 'caratteristica' è che la funzione di adattamento modella i dati di input entro una certa tolleranza di errore. Dal momento che non ho idea di cosa stai davvero facendo, sto inventando qualcosa; speriamo sia analgico.

Esempio di storia:

Come [pilota dell'X-Wing] voglio [non più dello 0,0001% errore di adattamento] in modo che [il computer di destinazione possa colpire la porta di scarico della Morte Nera quando si muove a tutta velocità attraverso un canyon box]

Quindi vai a parlare con i piloti (e con il computer di destinazione, se senziente). Prima parli di ciò che è "normale", poi parli dell'anormale. Scopri cosa conta davvero in questo scenario, cosa è comune, cosa è improbabile e cosa è semplicemente possibile.

Diciamo che normalmente avrai una finestra di mezzo secondo su sette canali di dati di telemetria: velocità, inclinazione, rollio, imbardata, vettore target, dimensione target e velocità target, e che questi valori saranno costanti o cambieranno linearmente. Anormalmente potresti avere meno canali e / o i valori potrebbero cambiare rapidamente. Quindi insieme ti vengono in mente alcuni test come:

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

Ora, potresti aver notato che non esiste uno scenario per la situazione particolare descritta nella storia. Si scopre, dopo aver parlato con il cliente e le altre parti interessate, che l'obiettivo nella storia originale era solo un esempio ipotetico. I veri test sono usciti dalla discussione che ne è seguita. Questo può succedere. La storia deve essere riscritta, ma non deve essere [poiché la storia è solo un segnaposto per una conversazione con il cliente].


5

Crea test per casi angolari, come un set di test contenente solo il numero minimo di input (possibile 1 o 0) e alcuni casi standard. Tali test unitari non sostituiscono test di accettazione approfonditi, né dovrebbero esserlo.


5

Ho visto molti casi in cui le persone investono moltissimo sforzo nel scrivere test per il codice che viene inserito raramente e non nel scrivere test per il codice inserito frequentemente.

Prima di sederti per scrivere qualsiasi test, dovresti guardare una sorta di grafico di chiamata, per assicurarti di pianificare una copertura adeguata.

Inoltre, non credo nello scrivere test solo per il gusto di dire "Sì, lo testiamo". Se sto usando una libreria che viene rilasciata e rimarrà immutabile, non sprecherò un giorno a scrivere test per assicurarmi che le interiora di un'API che non cambieranno mai funzionino come previsto, anche se alcune parti di esso segnano in alto su un grafico di chiamata. I test che consumano detta libreria (il mio codice) lo indicano.


ma che succede in un secondo momento quando la libreria ha una versione più recente con una correzione di bug?

@ Thorbjørn Ravn Andersen - Dipende dalla libreria, da cosa è cambiato e dal proprio processo di test. Non scriverò test per il codice che conosco funzioni quando l'ho lasciato cadere sul posto e non lo tocco mai. Quindi, se funziona dopo l'aggiornamento, è ovvio che va :) Naturalmente ci sono delle eccezioni.
Tim Post

se dipendi dalla tua biblioteca, il minimo che puoi fare è scrivere dei test che mostrino ciò che ti aspetti che detta biblioteca faccia effettivamente ,

... e se ciò è cambiato, test su cose che consumano detta libreria ... tl; dr; Non ho bisogno di testare le viscere del codice di terze parti. Risposta aggiornata per chiarezza, però.
Tim Post

4

Non proprio TDD, ma dopo essere entrato nel QA puoi migliorare i tuoi test impostando casi di test per riprodurre eventuali bug che emergono durante il processo di QA. Questo può essere particolarmente utile quando stai per ricevere supporto a lungo termine e inizi a raggiungere un luogo in cui rischi che le persone reintroducano inavvertitamente vecchi bug. Avere un test in atto per catturare è particolarmente prezioso.


3

Provo ad avere ogni test solo una cosa. Provo a dare ad ogni test un nome come shouldDoSomething (). Provo a testare il comportamento, non l'implementazione. Metto alla prova solo metodi pubblici.

Di solito ho uno o alcuni test per il successo, e quindi forse una manciata di test per fallimento, secondo il metodo pubblico.

Uso molto i modelli. Un buon mock-framework sarebbe probabilmente molto utile, come PowerMock. Anche se non lo sto ancora usando.

Se la classe A utilizza un'altra classe B, aggiungerei un'interfaccia, X, in modo che A non utilizzi direttamente B. Quindi creerei XMockup mock-up e lo userei al posto di B nei miei test. Aiuta davvero ad accelerare l'esecuzione del test, riducendo la complessità del test e riduce anche il numero di test che scrivo per A poiché non devo affrontare le peculiarità di B. Posso ad esempio testare che A chiama X.someMethod () invece di un effetto collaterale della chiamata a B.someMethod ().

Mantieni pulito anche il tuo codice di prova.

Quando si utilizza un'API, come un livello di database, lo deriderei e consentirei al mock-up di generare un'eccezione a ogni possibile opportunità a comando. Quindi eseguo i test uno senza lanciare, e in un ciclo, ogni volta lanciando un'eccezione alla prossima opportunità fino a quando il test non si ripete. Un po 'come i test di memoria disponibili per Symbian.


2

Vedo che Andry Lowry ha già pubblicato le metriche dei test unitari di Roy Osherove; ma sembra che nessuno abbia presentato il set (gratuito) che lo zio Bob dà in Clean Code (132-133). Usa l'acronimo PRIMO (qui con i miei riassunti):

  • Veloce (dovrebbero correre velocemente, quindi alla gente non dispiacerà correre)
  • Indipendente (i test non devono essere configurati o smontati l'uno per l'altro)
  • Ripetibile (dovrebbe funzionare su tutti gli ambienti / piattaforme)
  • Auto-validante (completamente automatizzato; l'output dovrebbe essere "pass" o "fail", non un file di registro)
  • Puntuale (quando scriverli, poco prima di scrivere il codice di produzione che testano)
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.