In tutti gli ambienti di sviluppo, qualsiasi sia il processo di produzione del codice adottato, uno degli aspetti considerati poco importanti è la fase di testing. Spesso sottovalutata per mancanza di tempo o per assenza di volontà da parte degli sviluppatori o semplicemente perchè considerata di scarso interesse. D'altra parte la fase di testing ha uno spiacevole inconveniente: viene subito prima della release. Quindi quando ormai le scadenze sono prossime (o, molto più spesso superate) e il committente è scalpitante per vedere all'opera il prodotto finito e funzionante.
Nell'articolo ci occuperemo di test, prima da un punto di vista teorico, poi con un esempio pratico di unit testing sviluppato con il tool sicuramente più utilizzato in ambiente Java: Junit (versione 4).
Test di unità e Test Driven Development
Una buona norma (best practice) che ogni sviluppatore dovrebbe effettuare è quello di eseguire test di unità per assicurarsi che la singola unità di sviluppo assolva le sue funzioni seguendo i requisiti. Questo è uno dei più importanti passaggi per poter avere un prodotto finale di buona qualità grazie ad un processo rapido di integrazione del software. Basta riflettere sull'idea di stratificazione del software per immaginare quanto sia complesso il processo di debug se qualcosa va storto nelle unità di base.
Il test di unità può essere sviluppato in maniera indipendente dallo sviluppatore, ma è buona norma avere un prodotto standard (se non altro a livello della stessa azienda) e aderire ad una pratica comune. Sicuramente la cosa più banale che possa esistere (ma non sottovalutatene l'importanza) è testare la singola classe con un main che valuti la bontà dei metodi. Questa pratica, seppure buona nelle intenzioni, ha delle evidenti limitazioni, come quella di non poter essere ripetuta nel tempo (ad esempio quando cambia un modulo del software) per i cosiddetti test di regressione (regression tests).
A tal proposito qualche anno fa (un bel po', oramai) Eric Gamma (uno dei famosi quattro dei Design Pattern) insieme a Kent Beck (creatore dell'Extreme Programming) iniziarono il progetto JUnit il cui scopo è quello di avere un framework per sviluppare test di unità secondo un semplice modello.
I cicli di sviluppo classici dell'ingegneria del software vanno quasi scomparendo soppiantati dall'utilizzo sempre più massiccio di metodologie agili (il cosiddetto Agile Programming, XP, ecc) il cui beneficio è quello di snellire le classiche fasi di analisi/sviluppo/test soprattutto in ambito Web based e in un contesto di mercato dinamicissimo.
Non entreremo nel merito di questi cicli di produzione poiché, sebbene legati al contesto, esulano dalla discussione, ma è interessante parlare della tecnica nota come Test Driven Development. Il TDD è un semplice modello di sviluppo che propone la scrittura dei test di unità prima ancora di sviluppare il codice. In questo modo, analizzando i requisiti e le esigenze del software si potrà giungere in una serie di iterazioni al prodotto finale.
In alcuni contesti ha sicuramente una sua utilità, ma probabilmente non se slegato da una buona fase di analisi dei requisiti e di disegno dell'applicazione.
JUnit 4: Annotation vs Reflection
Un test di unità è una semplice prova fatta per verificare che un requisito sia soddisfatto dal codice che abbiamo scritto. L'idea dello unit test in Java è quella di valutare ogni singolo metodo in funzione dei valori attesi.
Ipotizziamo la situazione in cui abbiamo creato una funzione matematica che converta un numero positivo in un numero negativo. Un caso di test (test case) verificherà che, quando passiamo il valore 5
, il risultato sia -5
. Se il test ha esito negativo evidentemente c'è un errore da correggere.
Abbiamo visto in teoria cosa siano i test di unità e la loro valenza in un ambiente di sviluppo. Iniziamo a prendere confidenza con JUnit, il tool prediletto da molti per l'automazione dei test di unità. A partire dalla versione 4, si avvale delle novità apportate da Java 5 (annotations) per rendere lo sviluppo di test suite davvero semplice ed immediato.
Nelle vecchie versioni creare uno unit test era comunque semplice ma bisognava conoscere la struttura della classe da estendere e per convenzione chiamare i metodi di test (i test case) testXXX
in modo che potessero essere eseguiti automaticamente (per reflection). Con le annotazioni tutto ciò si può facilmente evitare in quanto annotiamo ogni singolo metodo che desideriamo faccia parte dei nostri test case e, a runtime, le annotazioni verranno valutate.
Il processo di esecuzione quindi è guidato dalle annotazioni che vediamo di seguito:
Annotazione | Utilizzo |
---|---|
@Test | per annotare i metodi di test |
@Before | per annotare un metodo da eseguire prima dell'esecuzione di un test case (ad esempio l'apertura di una connessione ad un database, o uno stream) |
@After | per annotare un metodo da eseguire dopo l'esecuzione di un test case (chiusura di risorse aperte in precedenza) |
@BeforeClass | come @Before ma solo all'inizio dell'esecuzione del test |
@AfterClass | come @After ma solo alla fine dell'esecuzione del test |
@Ignore | utilizzata per evitare l'esecuzione di un test (evitando di commentare) |
L'annotazione @Test
può essere parametrizzata, e nell'esempio che segue discuteremo i casi. Vedremo un semplice caso di test la cui esecuzione evidenzia l'ordine di esecuzione dei metodi e le situazioni eccezionali che possono verificarsi.
package com.buongiorno.junit;
//import static
import static org.junit.Assert.*;
import org.junit.*;
public class UnitTest {
@BeforeClass
public static void initClass() {
System.out.println("initClass()");
}
@AfterClass
public static void endClass() {
System.out.println("endClass()");
}
@Before
public void initMethod() {
System.out.println("initMethod()");
}
@After
public void endMethod() {
System.out.println("end Method");
}
..//
Nella prima parte vediamo i metodi annotati come @Before(Class)
e @After(Class)
per vederne il punto di esecuzione durante il ciclo di vita del test.
La parte più interessante segue:
@Test
public void test1() {
System.out.println("Test 1");
assertTrue(true);
assertFalse(false);
}
@Test
public void test2() {
System.out.println("Test 2");
assertTrue(false);
}
@Test
public void test3() throws Exception {
System.out.println("Test 3");
throw new Exception();
}
@Test(timeout=100)
public void xyzTesting() throws InterruptedException {
System.out.println("Test xyzTesting");
//fallirà per timeout scaduto
Thread.sleep(200);
assertTrue(true);
}
@Test(expected=java.lang.Exception.class)
public void nuovoTest() throws Exception {
System.out.println("Test nuovoTest");
assertTrue(true);
throw new Exception();
}
}
I metodi test (annotati opportunamente dall'annotazione @Test
) verranno eseguiti senza un ordine preciso, quindi non necessariamente vengono eseguiti nell'ordine definito. Un test fallisce se fallisce almeno uno dei metodi assert contenuti in esso. I metodi assert sono metodi statici contenuti nella classe org.junit.Assert (abbiamo effettuato un import statico in testa alla classe) che effettuano una semplice comparazione tra il risultato atteso ed il risultato dell'esecuzione. In questo caso non stiamo testando nulla di concreto, semplicemente utilizziamo gli assertTrue
e assertFalse
su dei valori statici. In realtà guardando il set di metodi a disposizione ce ne sono decine da utilizzare in base alle vostre esigenze.
Un test potrebbe sollevare una eccezione. Quindi testare la robustezza di un'unità significa forzatamente sollevare un'eccezione. In questi casi è interessante l'uso del parametro "expected" nell'annotazione (nel codice è il metodo nuovoTest()
) dove diciamo che è attesa l'eccezione java.lang.Exception. Se il metodo non solleva quel tipo di eccezione il test sarà fallito.
Altro interessante parametro (metodo xyzTesting()
) è la definizione del timeout. Per esempio se state effettuando programmazione real time o un accesso ad un database che non deve superare una soglia di tempo, possiamo definire il tempo utile, oltre il quale il test non verrà passato (come nel caso d'esempio, dove simuliamo il ritardo con uno sleep()
maggiore del timeout).
Come abbiamo visto, definire dei test di unità è molto facile. Per eseguire il test (e per compilarlo) avete bisogno della libreria di Junit che trovate disponibile al download al sito www.junit.org. La classe main di Junit si aspetta come parametro la classe di test (quella che abbiamo scritto). Se sviluppate con Eclipse, avrete direttamente a disposizione questa libreria ed un plugin con cui potrete effettuare direttamente i test dalla console di Eclipse con una visualizzazione grafica dei test passati e dei test falliti.