MokaByte 99 - 7mbre 2005
 
MokaByte 99 - 7mbre 2005 Prima pagina Cerca Home Page

 

 

 

Pratiche di sviluppo del software
VI parte: Code Coverage

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

Introduzione
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.



Figura 1: Audit Code

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.


Figura 2: Test funzionali

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 <b>non</b> 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%



Figura 3: Report Emma di Code Coverage

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("")).



Figura 4: Il dettaglio del Code Coverage delle linee di codice

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.


Figura 5:
Audit Code + Test Funzionali + Code Coverage

 

Bibliografia
[MOKA_TDD] S. Rossini, Pratiche di sviluppo del software (I): Test Driven Development, MokaByte N.86 - Giugno 2004
[MOKA_QS_1] S. Rossini: Qualità del software: auditing del codice- Mokabyte N. 90 - Novembre 2004
[MOKA_QS_2] S. Rossini: Qualità del software(II): i Test- Mokabyte N. 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, Ed.Addison Wesley
[TFD] Michael Feathers : Test First Design -  http://www.xprogramming.com/xpmag/test_first_intro.htm
[SSJCC] Selection dei tools di code coverage per java -  http://europetravelogue.com/blog/pivot/entry.php?id=41
[JUNIT] http://www.junit.org/index.htm

 

Risorse
L'esempio mostrato in questo articolo è scaricabile qui