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
|