Una delle caratteristiche di un buon test di unità è che esso possa fallire per un singolo motivo. Ad esempio, un test riguardante un metodo di serializzazione di un oggetto di tipo User
che verifichi sia che la mail sia presente nella serializzazione, sia che la password sia invece nascosta, può fallire sia se la mail non è presente, sia se la password invece è presente. Sarebbe preferibile quindi sostituirlo con 2 test, uno per ciascun campo da controllare (o, in alternativa, usare lo snapshot testing).
Il motivo per cui sarebbe bene attenersi a tale regola è che così facendo si può in generale verificare un maggior numero di condizioni contemporaneamente. Se si usano più asserzioni nello stesso test, e la prima fallisce, non è possibile verificare l’esito delle successive fintanto che non si risolve il problema che ha causato il fallimento della prima. Distribuendo invece le asserzioni in più test si possono rilevare in un unico colpo tutti i problemi.
La regola precedente viene spesso interpretata come la necessità di usare una e una sola asserzione per test. E’ importante notare che l’uso di una singola asserzione contenente un’espressione booleana con operatori short-circuit (and, or, “null conditional”, ecc. nella maggior parte dei linguaggi) equivale di fatto a più asserzioni.
Quando trasgredire alla regola
Rispettare la regola della singola asserzione comporta un aumento dei costi di sviluppo e manutenzione dei test, a causa del maggior numero, appunto, di test (a tal proposito è importante strutturare bene il codice dei test, ad esempio estraendo il codice in comune tra più test). Talvolta si può tuttavia trasgredire alla regola senza particolari conseguenze negative, o addirittura con dei benefici.
Ecco alcuni casi:
- Condizioni che dipendono l’una dall’altra: ad esempio, se si vuole testare che un metodo restituisca una lista non vuota, ha senso avere due asserzioni nello stesso test, la prima che controlli che la lista non sia
null
e la seconda che controlli che non sia vuota. Se la lista ènull
è inutile proseguire col controllo successivo, e, allo stesso tempo, avere quella prima asserzione consente di ottenere un messaggio d’errore più significativo come risultato del test; - Framework che mettono a disposizione asserzioni non bloccanti: ad esempio, GoogleTest distingue fra asserzioni e “expectation”. Queste ultime quando falliscono non interrompono l’esecuzione del test, ma salvano comunque l’errore nel risultato. Quindi nella stessa esecuzione si possono rilevare anche fallimenti dei controlli successivi al primo che fallisce;
- Soluzioni ad hoc: ad esempio, “fluent assertion” (e.g.
Assert.That(myList).IsNotNull().And().IsNotEmpty()
) che valutano le varie condizioni senza corto circuitare.
Infine, la regola della singola asserzione è meno importante nei test di integrazione, e solitamente non si applica nei test end-to-end, perché trattandosi solitamente di scenari complessi da implementare e onerosi da eseguire, i costi aggiuntivi necessari per attenersi alla regola spesso sono molto maggiori dei benefici. Tanto più che la maggior parte delle funzionalità e delle casistiche dovrebbero essere coperte comunque dai test di unità.
Conclusioni
La regola della singola asserzione per test è molto conosciuta nell’ambito degli unit test. Talvolta viene vista come un dogma intoccabile. Applicando però il buon senso e un’analisi costi-benefici, tuttavia, talvolta è evidente che sia meglio trasgredire alla regola, che rimane comunque valida nella maggior parte dei casi.
Per approfondimenti o consulenze in ambito testing e quality assurance, scrivere a info@dvisentin.com.