Le affermazioni o i test unitari sono più importanti?


51

Sia le affermazioni che i test unitari servono come documentazione per una base di codice e un mezzo per scoprire i bug. Le differenze principali sono che le asserzioni funzionano come controlli di integrità e vedono input reali, mentre i test unitari vengono eseguiti su input simulati specifici e sono test su una singola "risposta corretta" ben definita. Quali sono i meriti relativi dell'utilizzo di assert vs. unità test come mezzo principale per verificare la correttezza? Quale credi dovrebbe essere enfatizzato più pesantemente?


4
Mi piace molto questa idea ... così tanto che sto facendo dell'argomento un compito per il mio test del software e il corso di garanzia della qualità. :-)
Macneil,

Un'altra domanda è: dovresti testare le tue affermazioni? ;)
mojuba,

Risposte:


43

Gli asserti sono utili per parlarti dello stato interno del programma . Ad esempio, che le strutture dei dati abbiano uno stato valido, ad esempio, che una Timestruttura di dati non detenga il valore di 25:61:61. Le condizioni controllate dalle asserzioni sono:

  • Presupposti, che assicurano che il chiamante mantenga il suo contratto,

  • Postcondizioni, che assicurano che la chiamata mantenga il suo contratto, e

  • Invarianti, che assicurano che la struttura dei dati abbia sempre delle proprietà dopo il ritorno della funzione. Un invariante è una condizione che è una precondizione e una postcondizione.

I test unitari sono utili per parlarti del comportamento esterno del modulo . Potresti Stackavere uno stato coerente dopo push()che è stato chiamato il metodo, ma se la dimensione dello stack non aumenta di tre dopo che è stato chiamato tre volte, questo è un errore. (Ad esempio, il caso banale in cui l' push()implementazione errata controlla solo le affermazioni e le uscite.)

A rigor di termini, la principale differenza tra assert e unit test è che i test unitari hanno dati di test (valori per far funzionare il programma), mentre gli assert no. Cioè, puoi eseguire i test unitari automaticamente, mentre non puoi dire lo stesso per le asserzioni. Per motivi di questa discussione, ho assunto che tu stia parlando dell'esecuzione del programma nel contesto di test funzionali di ordine superiore (che eseguono l'intero programma e non guidano moduli come unit test). Se non stai parlando di test funzionali automatizzati come mezzo per "vedere input reali", allora chiaramente il valore risiede nell'automazione e quindi i test unitari vincerebbero. Se ne stai parlando nel contesto di test funzionali (automatizzati), vedi sotto.

Ci può essere qualche sovrapposizione in ciò che viene testato. Ad esempio, Stackil postcondition di a potrebbe effettivamente affermare che la dimensione dello stack aumenta di uno. Ma ci sono limiti a ciò che può essere eseguito in tale asserzione: dovrebbe anche verificare che l'elemento superiore sia ciò che è stato appena aggiunto?

Per entrambi, l'obiettivo è aumentare la qualità. Per i test unitari, l'obiettivo è trovare i bug. Per asserzioni, l'obiettivo è quello di rendere più semplice il debug osservando gli stati di programma non validi non appena si verificano.

Si noti che nessuna delle due tecniche verifica la correttezza. In effetti, se si eseguono test di unità con l'obiettivo di verificare che il programma sia corretto, è probabile che si verifichino test poco interessanti che si sa funzioneranno. È un effetto psicologico: farai qualunque cosa per raggiungere il tuo obiettivo. Se il tuo obiettivo è trovare bug, le tue attività lo rifletteranno.

Entrambi sono importanti e hanno i loro scopi.

[Come nota finale sulle asserzioni: per ottenere il massimo valore, è necessario utilizzarle in tutti i punti critici del programma e non in alcune funzioni chiave. Altrimenti, la fonte originale del problema potrebbe essere stata mascherata e difficile da rilevare senza ore di debug.]


7

Quando si parla di affermazioni, tenere presente che possono essere disattivate con un semplice tocco di un interruttore.

Esempio di affermazione molto negativa:

char *c = malloc(1024);
assert(c != NULL);

Perché è così male? Poiché non viene eseguito alcun controllo degli errori se tale affermazione viene ignorata a causa della definizione di NDEBUG.

Un unit test (probabilmente) sarebbe semplicemente segfault sul codice sopra. Certo, ha fatto il suo lavoro dicendoti che qualcosa è andato storto o no? Quanto è probabile malloc()che fallisca sotto il test?

Le asserzioni sono a scopo di debug quando un programmatore deve implicare che nessun evento "normale" causerebbe l'attivazione dell'affermazione. malloc()il fallimento è, in effetti, un evento normale, quindi non dovrebbe mai essere affermato.

Esistono molti altri casi in cui vengono utilizzate le asserzioni invece di gestire adeguatamente le cose che potrebbero andare storte. Questo è il motivo per cui le asserzioni hanno una cattiva reputazione e perché lingue come Go non le includono.

I test unitari sono progettati per dirti quando qualcosa che hai cambiato ha rotto qualcos'altro. Sono progettati per salvarti (nella maggior parte dei casi) dall'esaminare tutte le funzionalità del programma (tuttavia, i tester sono importanti per le versioni) ogni volta che fai una build.

Non c'è davvero alcuna netta correlazione tra i due, tranne che entrambi ti dicono che qualcosa è andato storto. Pensa a un'affermazione come a un punto di interruzione in qualcosa a cui stai lavorando, senza dover usare un debugger. Pensa a un unit test come a qualcosa che ti dice se hai rotto qualcosa a cui non stai lavorando.


3
Ecco perché le asserzioni devono sempre essere affermazioni vere, non test di errore.
Dominique McDonnell l'

@DominicMcDonnell Bene, le dichiarazioni "dovrebbero essere vere". A volte asserisco di aggirare le stranezze del compilatore, come alcune versioni di gcc che hanno un buggy abs () incorporato. La cosa importante da ricordare è che le build di produzione dovrebbero averle disattivate comunque .
Tim Post

E qui credo che il codice di produzione è il luogo che ha bisogno del asserisce il più , perché è in produzione in cui si otterrà input che non hai pensato possibile. La produzione è il luogo che scaccia tutti i bug più difficili.
Frank Shearar,

@ Frank Shearar, molto vero. Dovresti comunque avere un errore grave al primo stato di errore rilevato in produzione. Sicuramente gli utenti si lamenteranno, ma è l'unico modo per assicurarsi che i bug vengano corretti. Ed è molto meglio ottenere un blah era 0 rispetto a un'eccezione di memoria per il dereferenziamento di null alcune chiamate di funzione in seguito.
Dominique McDonnell l'

perché non è meglio gestire problemi tipici ma spesso non comuni (agli occhi dell'utente) piuttosto che affermare che non dovrebbero mai accadere? Non riesco a realizzare questa saggezza. Certo, asserisci che un generatore di numeri primi restituisce un numero primo, che richiede un po 'di lavoro extra, che è previsto nelle build di debug. Affermare qualcosa che una lingua può testare nativamente è semplicemente, beh, stupido. Non racchiudere quei test in un altro interruttore che può essere spento qualche mese dopo un rilascio, ancora più stupido.
Tim Post

5

Sono entrambi strumenti usati per aiutare a migliorare la qualità generale del sistema che stai costruendo. Molto dipende dalla lingua che stai usando, dal tipo di applicazione che stai costruendo e da dove trascorri il tuo tempo. Per non parlare del fatto che ci sono alcune scuole di pensiero a riguardo.

Per iniziare, se stai usando una lingua senza una assertparola chiave, non puoi usare assert (almeno non nel modo in cui stiamo parlando qui). Per molto tempo, Java non aveva una assertparola chiave, e un certo numero di lingue ancora non ce l'hanno. Il test unitario diventa quindi un po 'più importante. In alcune lingue le asserzioni vengono eseguite solo quando è impostato un flag (di nuovo con Java qui). Quando le protezioni non sono sempre presenti, non è una funzione molto utile.

C'è una scuola di pensiero che dice che se si sta "Affermi" qualcosa, si potrebbe anche scrivere un if/ throwblocco di eccezioni significative. Questo processo di pensiero deriva da molti asserzioni poste all'inizio di un metodo per garantire che tutti i valori siano nei limiti. Testare i tuoi presupposti è una parte molto importante di avere un postcondizionamento atteso.

Il test unitario è un codice aggiuntivo che deve essere scritto e gestito. Per molti questo è uno svantaggio. Tuttavia, con l'attuale ritaglio di framework di unit test disponibili, è possibile generare un numero maggiore di condizioni di test con un codice relativamente piccolo. Test e "teorie" parametrizzati eseguiranno lo stesso test con un gran numero di campioni di dati che possono scoprire alcuni bug difficili da trovare.

Personalmente scopro di avere più chilometraggi con i test unitari rispetto alle asserzioni di aspersione, ma ciò è dovuto alle piattaforme che sviluppo la maggior parte del tempo (Java / C #). Altre lingue hanno un supporto dell'asserzione più solido e persino "Design by Contract" (vedi sotto) per fornire ancora più garanzie. Se stessi lavorando con una di queste lingue, potrei usare il DBC più del test unitario.

http://en.wikipedia.org/wiki/Design_by_contract#Languages_with_native_support

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.