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
|