MokaByte 91- Dicembre 2004 
Qualità del software
II parte il Test
di
Stefano Rossini

Nello scorso articolo ci siamo occupati di una possibile modalità per misurare la qualità del codice software limitatamente agli aspetti sintattici e stilistici (vedere [MOKA_QS_1]). In questo articolo vedremo come verificare le "qualità esterne" di Correttezza, funzionamen-to e Robustezza mediante i test.

Il concetto di qualità
Come già detto nello scorso articolo, la qualità del software è determinata da una combina-zione di diversi fattori.
Per qualità esterne si intendono quelle caratteristiche che possono essere apprezzate da un utente del software. Due di queste caratteristiche sono la Correttezza di funzionamento e la Robustezza.
La correttezza è la capacità di un software di fare esattamente quello per cui è stato creato, cioè, la conformità ai requisiti software. In altre parole: la correttezza di funzionamento si ha quando il prodotto software soddisfa le condizioni d'uso previste dalle specifiche.
La robustezza è invece la capacità del software di reagire "bene" a stimoli anomali. Un sof-tware è robusto quando ad esempio non va in "crisi" per un banale errore di battitura o un valore errato di tipo e/o di valore (supposto che questi non rientrino nelle specifiche), ma permette comunque di proseguire l'operatività e di segnalare/tracciare la situazione anomala riscontrata.
Un SW robusto "regge bene" anche in casi non esplicitamente previsti nei requisiti (e ce ne sono quasi sempre) permettendo quindi di gestire, rilevare e segnalare la situazione anomala riducendo i tempi, gli sforzi e i costi dell'attività di bug-finding.
L'unione della Correttezza e della Robustezza generalmente viene chiamata Affidabilità.


Figura 1 - Correttezza di funzionamento e Robustezza del SW


Nello scorso articolo si è parlato di qualità a livello di codice e si è visto come, applicando delle regole di audit sul codice prodotto, è possibile migliorarne la qualità nel rispetto delle Code Convention proposta e/o Linee guida.

Rispetto alla versione iniziale, il codice della classe d'esempio EuroConverter risulta di qualità migliore rispetto alle Code Convention.

/**
* <p>Title: EuroConverter</p>
* <p>Description: Classe che gestisce la conversione €uro in £ire</p>
* <p>Copyright: Copyright (c) 2001</p>
* <p>Company: Mokabyte</p>
* @author S. Rossini - srossini@mokabyte.it
* @version 1.0
*/
public class EuroConverter{
/** Valore in lire dell'euro */
private static final double EURO = 1936.27;
private EuroConverter(){}
/** Converte le lire in euro
* @param somma il valore delle £ire da convertire
* @return il corrispondente valore in €uro
* @throws ConversionException se la somma <b>non</b> e' un valore valido
*/
public static double convertiLireInEuro(String somma) throws ConversionException{
if(somma.equals("")) {
String errMsg = "convertiLireInEuro: ERRORE ! Somma NON valida!";
System.out.println(errMsg);
throw new ConversionException(errMsg);
}
double valoreSomma=0;
try {
valoreSomma=Double.parseDouble(somma);
}
catch (NumberFormatException nfe){
nfe.printStackTrace();
}
return valoreSomma/EURO;
}
}

Ovviamente un codice scritto bene non è detto che sia funzionalmente corretto e robusto.

Com'è possibile verificare/certificare la correttezza del funzionamento della classe?
Una possibile risposta? Con i test!
Con i test funzionali ed i test di unità è possibile misurare/certificare la correttezza e la robu-stezza del prodotto SW.
I test funzionali hanno lo scopo di verificare il corretto funzionamento "ai morsetti" del si-stema (end-to-end) come "black box", mentre i test di unità hanno lo scopo di testare singole classi o sotto parti del sistema, sfruttando la conoscenza delle parti interne del sistema (test white box) garantendo una maggiore "copertura" del codice realizzato.
Generalmente la responsabilità dei test funzionali è del cliente, mentre quella dei test di unità è dello sviluppatore.
In XP i test funzionali (Functional Test) sono stati rinominati test di accettazione (Acceptan-ce Test vedere[XP_AT]) 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' vici-ne 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]).
Per esemplificare quanto enunciato, vediamo come verificare la Correttezza del funziona-mento e la Robustezza della nostra classe EuroConverter, realizzando una serie di classi test utilizzando JUnit (vedere [JUNIT]).


Figura 2 - Correttezza di funzionamento e Robustezza


Per verificare la correttezza di funzionamento, predisponiamo una serie di test che, a fronte di determinati parametri di ingresso (la somma in lire da convertire), verifichino che il risul-tato che si ottiene coincida con il risultato atteso (il corrispettivo valore in euro).


Figura 3 - Correttezza di funzionamento della classe EuroConverter

I parametri del test (dato di input e risultato atteso) saranno opportunamente esternalizzati in un file di properties "test.properties".
Nel caso specifico di questi test si verifica che, dati in ingresso i seguenti valori in lire: 1936.27, 19362.70, 0 e -1936.27, si ottengano rispettivamente i seguenti valori in euro: 1.0, 10.0, 0.0 e -1.0.
Il contenuto del file test.propeties è il seguente:

# TEST 1
LIRE_INPUT_1=1936.27
EURO_ATTESI_1=1.0
# TEST 2
LIRE_INPUT_2=19362.70
EURO_ATTESI_2=10.0
# TEST 3
LIRE_INPUT_3=0
EURO_ATTESI_3=0.0
# TEST 4
LIRE_INPUT_4=-1936.27
EURO_ATTESI_4=-1.0

La classe di test effettua la lettura del file "test.properties" mediante la classe TestConfigura-tion ed invoca il metodo EuroConverter.convertiLireInEuro() con il dato di input (LI-RE_INPUT_N) letto dal file di test, verificando che il risultato ottenuto coincida con il risul-tato atteso (EURO_ATTESI_N).

public class ConverterTest extends TestCase {

private String datoInputTest; // dato di input del test
private double risultatoTestAtteso; // risultato atteso del test
private int testIndex; // indice del test

public ConverterTest2(String name, int index) {
super(name);
this.testIndex=index;
}

public static Test suite(){
int i=0;
TestSuite suite= new TestSuite("Converter Test");
suite.addTest(new ConverterTest2("testConverti", ++i));
suite.addTest(new ConverterTest2("testConverti", ++i));
suite.addTest(new ConverterTest2("testConverti", ++i));
suite.addTest(new ConverterTest2("testConverti", ++i));
return suite;
}

public void setUp(){
try{
TestConfigurator testConfig= TestConfigurator.getInstance("test.properties");
datoInputTest = testConfig.getProperty("LIRE_INPUT_"+this.testIndex);
risultatoTestAtteso = testConfig.getPropertyDouble("EURO_ATTESI_"+this.testIndex);
}
catch(TestConfigException tce){
fail(tce.getMessage());
}
}

/**
* Metodo di verifica della corretta funzionalità di conversione £ire in €uro
*/
public void testConverti(){
try{
double risultatoOttenuto=EuroConverter.convertiLireInEuro(this.datoInputTest);
assertEquals(this.risultatoTestAtteso,risultatoOttenuto,0);
}catch(Exception e){
fail("Exception: " +e.getMessage());
}
}

Lanciando i test otteniamo… verde!

I risultati che si ottengono (risultatoOttenuto) sono quelli attesi (this.risultatoAtteso) e il con-fronto tra i dati (assertEquals) del test è sempre verificato:

assertEquals(this.risultatoTestAtteso,risultatoOttenuto,0);

La classe, da un punto di vista funzionale, si comporta correttamente effettuando la giusta conversione della somma in lire nel corrispettivo in euro.

Ma come si comporterebbe se dovesse ricevere erroneamente in ingresso valori sbagliati co-me ad esempio stringhe non numeriche? La classe si "protegge" da un valore non corretto in input? Viene sollevata un'eccezione ConversioneExcpetion? Viene tracciata la situazione ano-mala?

Proviamo a rispondere a queste domande con una nuova serie di test con dati di input scor-retti (un reference String a null, una stringa di spazi , una stringa non numerica ed un nume-ro che al posto del punto usa la virgola come separatore) per verificare che la classe EuroCon-verter sollevi correttamente una ConversionException.


Figura 4
- Robustezza della classe EuroConverter

try{
double res=EuroConverter.convertiLireInEuro(<<INPUT_SCORRETTO>>);
fail("Non ho ottenuto l'eccezione!");
}catch(ConversionException ce){
System.out.println("Ottenuta ConversionException: " + ce.getMessage());
}catch(Exception e){
fail("Exception: " +e.getMessage());
}

Questi quattro nuovi test… falliscono tutti.


Figura 5 - I test JUnit


Il primo test fallisce con un'eccezione java.lang.NullPointerException, mentre gli altri falliscono perché non viene sollavata nessuna eccezione, ma la classe restituisce come valore della con-versione 0 euro.

Questo indica che la robustezza da parte del codice è sicuramente migliorabile.
Il primo test fallisce perché, dando in ingresso una reference String a null, si ottiene una NullPointerException. Questo è dovuto al seguente statement poco robusto:

if(somma.equals("")) {

se la variabile somma è null si ha una NullPointerException.

Procediamo quindi a cambiare lo statement scambiando i termini di comparazione:

if("".equals(somma)) {

Rilanciando il test… si ha ancora rosso! Si ottiene nuovamente un NullPointerException questa volta dovuto al fatto che il null si propaga fino all'istruzione
Double.parseDouble(somma) che ovviamente provoca una NullPointerException.

Dobbiamo quindi rendere più sicuri i controlli di validità del dato ricevuto in ingresso dal metodo convertiLireInEuro(). Aggiungiamo quindi anche il controllo sul valore null:

if("".equals(somma) || somma == null) {

In questo modo il test dà verde perché solleva giustamente, a fronte di un input null, una ConversionException.

Proviamo ora a capire come mai, data in ingresso una stringa di spazi, il test fallisce.
Il controllo iniziale sul valore somma di fatto si limita a verificare che la stringa sia vuota, ma non a verificare se la stringa contenga più spazi.

E' sufficiente irrobustire ulteriormente il controllo inserendo il trim() sul parametro somma dopo essersi ovviamente cautelati che il parametro somma non sia null:

if(somma == null || "".equals(somma.trim())) {


Concentriamoci ora sugli ultimi due test che falliscono quando in ingresso si ha una stringa non numerica o un numero con la virgola al posto del punto.
La classe EuroConverter se cattura una NumberFormatException nel metodo di conversione Double.parseDouble(), si limita a stampare lo stack dell'eccezione senza sollevare una Conver-sionException e restituisce il valore della variabile valoreSomma (inizializzata a 0).
Provvediamo quindi a rilanciare una ConversionException a fronte di un fallimento di con-versione da Sringa a double del metodo Double.parseDouble():

double valoreSomma=0;
try{
valoreSomma=Double.parseDouble(somma);
}
catch(NumberFormatException nfe){
String errMsg = "convertiLireInEuro: ERRORE ! Somma["+somma+"] da convertire NON
valida!";
System.err.println(errMsg); // es: Log4j
throw new ConversionException(errMsg);
}

Rilanciando i test otteniamo... verde !


Figura 6
- I test JUnit


Grazie ai test abbiamo modificato il codice originario rendendolo più robusto e miglioran-done la qualità del funzionamento a fronte di valore d'ingresso non corretti.

 

Conclusioni
In questi due articoli abbiamo visto come sia possibile migliorare la "qualità" in termini di codice (stile, sintassi e conformità a Code Convention) ed in termini di robustezza (test!).
Il semplice esempio proposto ha cercato di illustrare come un codice di partenza "scadente", sia migliorabile applicando sia la pratica di audit code che la pratica dei test funzionali e di unità.


Figura 7 - Audit Code + test JUnit applicati alla classe EuroConverter

 

Bibliografia e riferimenti
[MOKA_QS_1] S. Rossini: Qualità del software: auditing del codice - Mokabyte N. 90 - No-vembre 2004
[WK_FT] http://c2.com/cgi/wiki?FunctionalTest
[WK_AT] http://c2.com/cgi/wiki?AcceptanceTest
[XP_AT] http://www.extremeprogramming.org/rules/functionaltests.html
[JUNIT] http://www.junit.org/index.htm
[JMETER] http://jakarta.apache.org/jmeter


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it