Molti gestori di progetto dopo la messa in produzione di un nuovo software sono amareggiati dal presentarsi di una o più anomalie, di miscellanea gravità. La domanda che sorge spontanea è: “Perché abbiamo deployato software con bug?”.
E la risposta, a parte casi di conclamata noncuranza, è la seguente:
Produrre bug è come peccare: nessuno dovrebbe farlo, ma è insito nella natura umana.
Il peccato originale
La realtà è che nessun di team di sviluppo è esente dal produrre codice con anomalie. Insomma, la mela tentatrice alla fine è irresistibile per chiunque: noi sviluppatori spesso siamo affascinati dal lato oscuro del codice e infiliamo nuove feature quando dovremmo solo stabilizzare, ci arrampichiamo su pattern barocchi per il puro senso di auto soddisfazione (ah, vanità!), complichiamo invece di fattorizzare perché abbiamo fretta. Insomma: siamo esseri umani!
La soluzione a tutto questo non è quindi prevenire i bug, ma curarli: ogni fase di sviluppo deve essere seguita da una cospicua fase di test: questa fase è il purgatorio del programmatore e come il vero purgatorio dura in proporzione a quanto siamo stati cattivi.
I Gironi del purgatorio dei test
Ovviamente non è così semplice: infatti ci sono diversi tipi di test da fare, organizzati in fasi successive. Eseguite tutte le fasi previste possiamo sperare di ascendere alla produzione.
Primo girone: test di unità (Unity test)
Sono i test più semplici, ma non per questo meno importanti. In sostanza testano che una singola cosa funzioni. Esempi: questa query non dà errore, questo metodo fa quello che ci si aspetta. Sono test che scrive lo sviluppatore e dovrebbero essere conteggiati nella stima di ogni componente software.
Secondo girone: test funzionali
Dopo che la singola classe è stata testata tiriamo un sospiro di sollievo. Niente ormai ci preclude la vetta della beatitudine. Sbagliato! Sì, la query non scoppia e l’applicativo è usabile. Ma chi ci dice che i dati siamo corretti? Che gli use case siamo rispettati? Chi insomma ci garantisce che il software che abbiamo prodotto funzioni come atteso? Bisogna che qualcuno che sa come il software deve funzionare da un punto del business faccia dei test per noi, con alla mano l’analisi funzionale (l’avevate fatta, vero? Sennò sareste all’Inferno e non in Purgatorio!). E’ qui compare il nostro “Virgilio”: l’analista funzionale, che ci conduce per mano al terzo girone.
Terzo girone: test utente (Acceptance test)
Bene, ora siamo infine giunti sulla vetta del monte Purgatorio: qua c’è uno spiazzo detto “Ambiente di Certificazione”.
Ci si fa incontro una figura luminosa: l’utente! Prima di farci salire egli verificherà che il software funzioni come lui se lo aspettava. Voi direte: lui ha le stesse identiche aspettative dell’analista funzionale, quindi questa fase è una mera validazione. Nell’Eden sarebbe così: ma ricordate la mela? In realtà non è detto che l’utente utilizzi il software nel modo in cui l’analista funzionale si è prefigurato; inoltre l’ambiente di certificazione su cui deployamo il software può presentare alcune differenze con quello in cui abbiamo sviluppato: sistema operativo, versione del JDK, versione del DBMS. Ovviamente nell’Eden i 3 ambienti classici (sviluppo, certificazione e produzione) sarebbero identici e perfetti come le sfere celesti di Tolomeo, ma la perfezione non è di questo mondo e qualche differenza, se pur minima, esiste sempre. Insomma: il test utente è vitale affinché si consegni del codice che rispetta il mandato iniziale.
Integrazione finale
Se il software che dobbiamo realizzare è corposo abbiamo magari suddiviso in diversi rilasci: in questo caso, oltre a risalire il monte Purgatorio più volte, alla fine dobbiamo anche verificare che le varie tranche, che in isolamento funzionavano perfettamente, continuano a funzionare anche in integrazione. “Come è possibile che non sia così?” si chiederà l’ottimista…ricordate la mela?
JUnit
Focalizziamo ora l’attenzione sui test di unità. Per questi esiste, in ambito della programmazione Java, JUnit, il Santo Graal che ci mantiene sulla retta via. E’ un framework che ci consente di definire in modo semplice metodi per testare il nostro codice
La classe da testare
Supponiamo di voler testare la classe seguente:
package it.cosenonjaviste.junit;
public class MyExampleClass {
public int multiply(int x, int y) {
return x / y;
}
}
Prepariamoci
E’ buona norma creare i test di unità sotto una cartella separata rispetto a src. Creiamo quindi una nuova source folder "test" cliccando col tasto destro sul nostro progetto, selezionando “properties” e scegliendo "Java Build Path". Selezioniamo il tab “Source Code”. Premiamo "Add folder" e poi "Create new folder". Con fantasia scegliamo il nome della cartella: test!
Creiamo un JUnit Test
Facciamo click destro sulla nostra classe MyExampleClass
e selezioniamo New –> JUnit Test Case. Impostiamo le proprietà come mostrato nello screenshot.
Eclipse ci chiede se vogliamo aggiungere JUnit al build path. Rispondiamo di sì ed Eclipse imposterà quanto necessario per usare JUnit. Facile, no?
Ci viene anche chiesto quali metodi vorremo testare: nel nostro caso ne abbiamo uno solo, selezioniamo quindi il check accanto al metodo multiply.
package it.cosenonjaviste.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class MyExampleClassTest {
@Test
public void testMultiply() {
fail("Not yet implemented");
}
}
Eclipse ha generato la classe di test per noi, con una implementazione vuota: il metodo fail non fa altro che fallire il test con un messaggio; in questo caso il messaggio è che il test è ancora da implementare! Scriviamo quindi il nostro test: il metodo assertEquals
di JUnit verifica che il risultato di multiply
invocato con i parametri 10 e 5 sia 50. Se così non sarà il test fallirà.
public class MyExampleClassTest {
@Test
public void testMultiply() {
MyExampleClass tester = new MyExampleClass();
assertEquals("Result", 50, tester.multiply(10, 5));
}
}
Pronti a lanciare…
Per lanciare il test facciamo click destro sulla nostra classe di test e selezioniamo Run-As -> Junit Test.
Il test fallisce: infatti nel metodo multiply
abbiamo in realtà fatto una divisione! Sistemiamo il bug e rilanciamo il test: questo volta abbiamo una green light:
Le Test Suite
Mano a mano che il nostro progetto cresce abbiamo un numero crescente di classi e in teoria un numero crescente di classi di test (se non siamo stati dei pigroni!). Per questo motivo diventa indispensabile la possibilità di raggruppare i test in quelle che JUnit chiama Test Suite: un raggruppamento di test che, quando viene eseguito, lancia tutti i test in essa contenuti.
Allo scopo di mostrare una JUnit Test Suite creiamo:
- una nuova classe
MyExampleClass2
, con un metodo divide - una nuova classe tester di
MyExampleClass2
: la fantasia suggerisce di chiamarlaMyExampleClass2Test
package it.cosenonjaviste.junit;
public class MyExampleClass2 {
public int divide(int x, int y) {
return x / y;
}
}
package it.cosenonjaviste.junit;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class MyExampleClass2Test {
@Test
public void testDivide() {
MyExampleClass2 tester = new MyExampleClass2();
assertEquals("Result", 20, tester.divide(100, 5));
}
}
La nostra Suite la creiamo come segue:
package it.cosenonjaviste.junit;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({ MyExampleClassTest.class, MyExampleClass2Test.class })
public class AllTests {
}
Lanciandola possiamo vedere l’esito di tutti i test in essa inclusi:
Le Test Suite sono il caposaldo di sofisticati strumenti di Continuous Integration come Cruise Control, Hudson, Jenkis e altri.
Da notare che è possibile anche fare click di destro su un package e selezionare Run-As -> JUnit Test per lanciare tutti i JUnit Test e le Suite Test in esso contenuti.
Fare qualcosa prima e dopo i test
E se prima di ciascun metodo di test (annotato con @Test) di una classe volessi lanciare del codice di inizializzazione? O del codice di pulizia al termine del test?
E’ possibile utilizzare le annotation @Before
e @After
. Ecco un esempio:
package it.cosenonjaviste.junit;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class MyExampleClass2Test {
@Before
public void init() {
System.out.println("I'm about to test divide");
}
@Test
public void testDivide() {
MyExampleClass2 tester = new MyExampleClass2();
assertEquals("Result", 20, tester.divide(100, 5));
}
@After
public void clean() {
System.out.println("Cleaning after test divide");
}
}
Le corrispettive annotazioni per lanciare del codice prima e dopo tutti i metodi di una classe sono invece @BeforeClass
e @AfterClass
. Dar ricordare: se un test ha più metodi la classe di test viene istanziata una volta per ogni metodo. Per questo motivo @BeforeClasse @AfterClass devono essere posizionati su metodi statici!
E se il mio test ha successo se si verifica un’eccezione?
Le eccezioni checked sono utilizzate per modellizzare scenari alternativi degli use case. Per questo non è per nulla detto che se un test lancia un’eccezione il test sia fallito: in un applicativo di e-commerce un test potrebbe dover verificare che venga gestito il caso in cui il credito è minore del valore dell’oggetto acquistato; il test potrebbe avrebbe successo se viene lanciata l’eccezione CreditoInsufficienteException
. In questo caso basta utilizzare questa annotazione sul metodo in questione:
@Test(expected=CreditoInsufficienteException.class)
Non è possibile che JUnit abbia anche questo…
Ok, il codice che ho scritto fa quello che deve fare. Ma per me è un errore se un determinato metodo impiega più di 10 secondi a terminare l’elaborazione. In questo caso non posso usare JUnit? Sbagliato! C’è un attributo dell’annotation @Test
che fa al caso mio:
@Test(timeout = 10000)
Conclusioni
JUnit, pur essendo semplice da utilizzare, offre molta più flessibilità rispetto all’utilizzo di un metodo main utilizzato al fine di testare.
I vantaggi sono:
- Organizzazione: una classe di test per ogni classe da testare
- Integrazione con l’ide: click destro su un JUnit test per lanciarlo
-
Informazioni aggiuntive: viene mostrato il tempo di esecuzione del test. Inoltre non si ha un semplice “il codice scoppia/il codice non scoppia”; grazie a metodi assert è possibile ad esempio stabilire che il test fallisce se la query sulla tabella anagrafica degli utenti ne ha reperiti 2 anziché uno (che era il risultato atteso)
-
Test Suite: possibilità di organizzare i test in gerarchie e lanciarne molti in cascata
Quindi, se siamo finiti al Purgatorio, JUnit è lo strumento adatto per agevolare la nostra ascesa verso la luce della produzione senza bug!
Pingback: ()