Pratiche di sviluppo del software

Le misure di copertura sono importanti per definire il grado di affidabilità delle suite di test e quindi, indirettamente, per verificare la qualità del codice che viene testato tramite esse.

Introduzione

Nei precedenti numeri apparsi su MokaByte nella sezione Metodologia si è parlato di Test Driven Development (vedere [MOKA_TDD]) e di qualità  del Software (vedere [MOKA_QS_1] e [MOKA_QS_2]).

In [MOKA_QS_1]) si è parlato di qualità  a livello di codice e si è visto come, applicando delle regole di audit sul codice prodotto, sia possibile migliorarne la qualità  nel rispetto delle Code Convention e/o Linee guida proposte.

Si è visto come partendo da un codice di esempio che gestiva una semplice conversione da lire in euro, applicando le regole di audit definite da Sun (Sun Code Conventions) e utilizzando il prodotto Open Source Checkstyle (vedere[CHECKSTYLE]) il codice risultasse di bassa "qualità ":

Applicando le regole di audit sul codice, Checkstyle può creare un report HTML. Nel caso in esame si erano rilevate ben 11 violazioni delle Sun Code Convention.

Rimuovendo gli errori di codifica (code e naming convention disattese) il codice è risultato essere, dal punto di vista prettamente della qualità  sintattica e stilistica, migliore sia in termini di leggibilità  che di coerenza.

Ovviamente un codice che rispetta delle linee guida di codifica non è necessariamente corretto funzionalmente e robusto.

Com‘è possibile verificare/certificare la correttezza del funzionamento del codice?
Le metodologie di test possono dare un concreto aiuto in questa direzione.

Ad esempio, la presenza di test funzionali ed i test di unità  può dare una misura della correttezza e la robustezza del software.

I test funzionali hanno lo scopo di verificare il corretto funzionamento "ai morsetti" del sistema (end-to-end). Normalmente chi definisce questi test non conosce il comportamento "interno" del software, quindi questi test vengono disegnati seguendo una strategia "black box".
Viceversa, la strategia "white-box" presume che i test vengano definiti sulla base della conoscenza del funzionamento interno del sistema. I test di unità , il cui scopo è di verificare la correttezza di singole componenti del sistema, e che normalmente sono di responsabilità  degli sviluppatori, possono essere definiti utilizzando una strategia white-box, o un approccio "ibrido".

In XP i test funzionali (Functional Test) sono stati rinominati test di accettazione (Acceptance Test) per enfatizzare che i test devono dimostrare/garantire l‘aderenza del prodotto SW ai requisiti e di conseguenza mettere in grado il Customer di accettare o meno il prodotto SW mediante la definizione di precisi criteri di accettazione (Acceptance Criteria). Di fatto in XP si cerca di portare le tematiche/problematiche di Qualità  Assurance piu‘ vicine al gruppo di sviluppo di quanto non avvenga generalmente, nella maggior parte dei casi infatti il Q&A viene gestito da un team di lavoro indipendente e separato dallo sviluppo.

E‘ importante notare come gli stessi test di accettazione diventino anche importantissimi per verificare sistematicamente la non regressione del sistema (vedere [MOKA_TDD]). Questo è ovviamente vero anche per le suite di unit tests.

Quindi, mentre la qualità  dei test di accettazione è misurata implicitamente con la verifica dei requisiti del cliente, occorre definire una metodologia per misurare la qualità  delle suites di unit test. Questo è importante perchà© altrimenti non sappiamo quando possiamo "fidarci" di una suite di test o, in generale, non possiamo controllare quello che non riusciamo a misurare.

I parametri più comunemente utilizzati per la misura quantitativa della qualità  dei test sono le misure di copertura.

Esistono in letteratura varie tipologie e livelli di copertura misurabili. Ne descriveremo alcune fra le più comuni.

Tipologie e livelli di copertura del code coverage

  • Statement coverage

Conosciuto anche come "line coverage", è la misura definita dal numero di linee di codice eseguite dalla suite di test sul numero totale di linee. E‘ evidentemente la misura più semplice che si può effettuare. Misure simili (ma più grossolane) sono la copertura dei metodi (method coverage) o delle classi (class coverage).

  • Block coverage

Simile allo statement coverage, il block coverage considera ogni sequenza di codice "consecutiva" (cioè senza biforcazioni) come un blocco. Questo serve a considerare correttamente casi del tipo:

if (condizione) {99 righe di codice} else {1 riga di codice bacata...}

Un test con condizione = true copre le linee al 99% (che, guardando solo i numeri, è un ottimo risultato), ma solo un 50% sulla misura di block coverage...
Questo dimostra che lo statement coverage, il tipo di copertura più intuitivo, può essere fuorviante!

  • Branch coverage

Si possono considerare altri casi in cui lo statement coverage può dare dei risultati ingannevoli, ad esempio:

Integer var = null;if (condizione) {var = new Integer(0);} return var.toString();

Questo codice ha una misura di statement coverage al 100% (se condizione=true), ma se condizione=false, genera una NullPointerException.
Introduciamo quindi la misura di branch coverage, che ci dice quanti dei possibili rami del codice sono stati coperti. In realtà  questo tipo di misura non è frequente, in quanto è molto complesso riuscire a determinare tutti i possibili percorsi. Mentre per uno statement di if è semplice (ma in questo caso si riesce a comunque a determinare una copertura corretta anche tramite block coverage), non è sempre semplice / possibile determinare la copertura di cicli di cui non si conosce a priori il numero di esecuzioni. Da notare che nemmeno questo tipo di copertura (che normalmente non viene effettuato se non in casi semplici) protegge da alcune tipologie di errore....

  • Condition Coverage

Prendiamo ad esempio il pezzo di codice:

if ((somma != null) || (somma.equals(""))) {System.out.println("Somma non e‘ null");}

Se questo pezzo di codice viene eseguito da un test con somma != null, si ottiene 100% alla misura di statement coverage, block coverage e branch coverage...però la seconda condizione booleana non viene coperta e contiene un errore. La misura della percentuale di esecuzione delle condizioni è chiamata condition coverage.

Utilizzo delle misure di copertura

Un criterio quantitativo di copertura viene spesso utilizzato come soglia, (es. 90% degli statement) raggiunta la quale l‘attività  di testing viene considerata troppo costosa e viene quindi interrotta. E‘ chiaro che occorre raggiungere un giusto compromesso (dove "giusto" dipende da molti fattori, non ultima la criticità  del software prodotto) tra risultati di copertura e numero/qualità  dei test della suite.

Inlotre si possono usare queste misure per valutare l‘efficacia di una fase preliminare di testing funzionale e per capire se questa ha lasciato scoperte (non esercitandole) alcune strutture del programma.

Esempio

I concetti descritti in questo articolo sono stati provati tramite il tool OpenSource di misura della copertura "Emma", uno dei tanti disponibili (vedere bibligrafia).
Emma può fornire misure di copertura di classe, metodo, statement e blocchi. Inoltre può produrre dei report html in cui visualizza (con colori diversi) le righe non eseguite.
Le righe contenenti espressioni booleane in cui non tutte le condizioni sono state coperte sono rappresentate in un colore differente (giallo). Non viene quindi data una misura numerica di questo tipo di copertura ma le condizioni non testate possono essere comunque individuate.

Esemplificando quanto detto fino ad ora riprendiamo in esame l‘esempio presentato nei precedenti numeri ... un po‘ "triviale" ma (speriamo!) efficace.

Supponiamo di avere verificato il funzionamento della classe EuroConverter con la seguente classe di Test JUnit:

public class ConverterTestCase extends TestCase {protected void setUp() {}protected void tearDown() {}public ConverterTestCase(String string) {super(string);}public void testConvertiEuroInLire() {  try {double res=EuroConverter.convertiLireInEuro("1936.27");assertEquals("Non ho ottenuto la conversione prevista:","1.0",""+res);} catch (Exception ex) {fail(ex.getMessage());}} public static Test suite() {TestSuite suite = new TestSuite("Converter TestSuite");suite.addTest(new ConverterTestCase("testConvertiEuroInLire"));return suite;}}

dove la classe EuroConverter è la seguente:

public class EuroConverter{/** Valore in lire dell‘euro */private static final double EURO = 1936.27;private EuroConverter(){} // ... per didattica/** Converte le lire in euro * @param  somma il valore delle lire da convertire * @return il corrispondente valore in euro * @throws ConversionException se la somma non e‘ un valore valido */public static double convertiLireInEuro(String somma) throws Exception{System.out.println("EuroConverter.convertiLireInEuro: valore arrivato["+somma+"]...");if ((somma != null) || (somma.equals(""))) {System.out.println("Somma non e‘ null"); // ... per didattica }if(somma == null || "".equals(somma.trim())) { String errMsg = "convertiLireInEuro: ERRORE ! Somma["+somma+"] da convertire NON valida!";System.out.println(errMsg);throw new Exception(errMsg);}double valoreSomma=0;try{valoreSomma=Double.parseDouble(somma);}catch(NumberFormatException nfe){nfe.printStackTrace();throw new Exception("Conversione Double in Stringa fallita: " + nfe.getMessage()); }System.out.println("convertiLireInEuro: valore somma da convertire["+valoreSomma+"]...");return valoreSomma/EURO;}}

Eseguendo il test si ottiene un report HTML che riporta una copertura di classe del 100% (l‘unica classe è stata testata), una copertura di metodo del 50% (manca l‘esecuzione del costruttore della classe), una copertura di blocco del 60% e di linee del 53%

Andando in dettaglio sulla copertura della classe, è interessante notare che esistono due blocchi non coperti, (le righe non eseguite sono in rosso) e una condizione parzialmente coperta (in giallo) che nasconde un bug (con somma uguale a null si avrà  un NullPointerException dovuto alla seconda condizione in OR: somma.equals("")).

Da questo report si capisce come la classe di test non sia sufficiente.
Quantomeno bisogna aggiungere dei nuovi metodi di test che prevedano i casi di test con somma=null,somma valga stringa vuota e che somma contenga una stringa non numerica

Conclusioni

Le misure di copertura sono importanti per definire il grado di affidabilità  delle suites di test e quindi, indirettamente, la qualità  del software che viene da loro testato. Le metriche che sono state introdotte sono metriche strutturali, e la loro misura deve essere vista come complementare alla verifica di copertura funzionale da parte dei Functional Test.

In conclusione, abbiamo aggiunto, all‘audit del codice e al test funzionale, un nuovo strumento metodologico per aumentare la nostra confidenza sulla qualità  del codice prodotto.

Esempi

Dal menu in alto a sinistra, è possibile scaricare gli esempi appena visti

Riferimenti bibliografici

[MOKA_TDD]
S. Rossini, "Pratiche di sviluppo del software. Parte I: Test Driven Development", MokaByte 86, Giugno 2004

[MOKA_QS_1]
S. Rossini: "Qualità  del software.Parte I: Auditing del codice", Mokabyte 90, Novembre 2004

[MOKA_QS_2]
S. Rossini: "Qualità  del software. Parte II: I Test", Mokabyte 91, Dicembre 2004

[CCA]
S. Cornett, "Code Coverage Analysis"
http://www.bullseye.com/coverage.html#intro

[HTMCC]
B. Marick, "How to Misuse Code Coverage"
http://www.testing.com/writings/coverage.pdf

[AFIT]
Il Testing
http://www.dsi.unifi.it/~fantechi/INFIND/testing.ppt

[OSCCTJ]
Open Source Code Coverage Tools in Java
http://java-source.net/open-source/code-coverage

[EMMA]
http://emma.sourceforge.net

[SATDD]
Scott W. Ambler, "Test Driven Development"
http://www.agiledata.org/essays/tdd.html

[KBTDD]
Kent Beck, "Test Driven Development: By Example", Addison Wesley

[TFD]
Michael Feathers, "Test First Design"
http://www.xprogramming.com/xpmag/test_first_intro.htm

[SSJCC]
Selezione dei tools di code coverage per Java
http://europetravelogue.com/blog/pivot/entry.php?id=41

[JUNIT]
http://www.junit.org/index.htm

Condividi

Pubblicato nel numero
99 settembre 2005
Stefano Rossini è nato a Giussano (MI) il 29/10/1970 e ha conseguito il diploma universitario in Ingegneria Informatica presso il Politecnico di Torino. Ha maturato più di venti anni di esperienza in diversi progetti Enterprise mission-critical ricoprendo i ruoli di IT Program Manager, Project Manager & Software Architect presso importanti…
Marco Piraccini è nato a Cesena il 09/10/1975. E‘ laureato in Ingegneria Informatica presso la facoltà di Bologna con una tesi sull‘assessment della maturità del processo di testing e in Fisica Computazionale presso la facoltà di Udine con una tesi sull‘uso di GRID per le simulazioni Monte Carlo (nell‘ambito di…
Ti potrebbe interessare anche