ATTENZIONE: il nuovo sito di riferimento per questa guida e altre cose più
o meno interessanti è
il mio BLOG
Un modo ovvio per testare i metodi di una classe è quello di inserire
nel codice dei comandi che stampano il valore di determinate variabili,
e di controllare se questi valori sono consistenti.
Esiste un metodo automatico per verificare che certe condizioni siano
soddisfatte senza "inquinare" il codice con comandi di stampa:
usare JUnit.
Supponiamo di dover testare una classe con dei metodi che calcolano
il quoziente e il resto della divisione intera -
naturalmente una classe del genere ha solo un interesse "teorico", allo scopo
in java esistono già gli operatori /
e %
:
package algebra; public class Div { /** divisione intera per sottrazioni successive */ public static int div(int a, int b) { if( a == 0 || a < b ) return 0; return div( a-b, b ) + 1; } /** resto della divisione intera */ public static int mod(int a, int b) { if( a == 0 ) return 0; if( a < b ) return a; return mod( a-b, b ); } }
JUnit 4.0 fa uso delle annotazioni, un meccanismo introdotto in Java 5 che permette appunto di annotare il codice per fornire indicazioni ad altri programmi - ad esempio JUnit.
Una classe di test per la classe Div
si potrebbe scrivere così:
import algebra.*; // importo la classe Div import org.junit.*; import static org.junit.Assert.*; // static import 5 public class DivTest { @Test public void divisioni() { 10 assertTrue( Div.div(654, 321) == 2 ); assertTrue( Div.div(3, 2) == 1 ); assertTrue( Div.div(123456, 15) == 8230 ); assertTrue( Div.div(0, 741) == 0 ); } 15 ... }L'importazione della classe
algebra.Div
potrebbe
non essere necessaria.assertTrue
della classe
Assert
, che fa fallire il test nel caso la condizione non sia vera.
Gli static import dovrebbero comunque essere
usati solo sporadicamente, se effettivamente rendono il codice più leggibile.
In linea 8 è stato annotato con @Test
il metodo divisioni
, che controlla i risultati di alcune
invocazioni del metodo statico div
.
A questo punto basta eseguire org.junit.runner.JUnitCore
,
con parametri i nomi delle classi contenenti i metodi annotati come @Test
:
java org.junit.runner.JUnitCore DivTestche in questo caso scriverà:
JUnit version 4.0 . Time: 0 OK (1 test)Nel classpath deve essere presente il file
junit-4.0.jar
(in questo caso anche la directory che contiene algebra
,
per trovare la classe Div
).
Per poter eseguire i test scritti con la versione 4 di JUnit con un runner di versioni precedenti, come quelle incluse in Ant, Netbeans, eccetera, è necessario includere in ogni classe di test il seguente metodo "di adattamento":
public static junit.framework.Test suite() { return new junit.framework.JUnit4TestAdapter(DivTest.class); }avendo cura di sostituire
DivTest.class
con il nome della classe di test.
Proviamo ad aggiungere un metodo alla classe DivTest
:
... @Test public void divisioniPer0() { Div.div(0, 0); } ...questo non dovrebbe essere permesso, si tratta di una divisione per 0, eppure il test passa.
Il test che segue invece non passa:
... @Test public void divisioniPer0() { Div.div(5, 0); } ...ma solo perché la ricorsione non arriva mai al caso base e quindi lo spazio sullo stack finisce.
In entrambi i casi l'invocazione del metodo dovrebbe lanciare un'eccezione di "divisione per zero". Il meccanismo delle annotazioni permette di specificare dei parametri; in questo caso è possibile indicare a JUnit quale eccezione deve essere lanciata nel corpo del metodo di test:
... @Test (expected = ArithmeticException.class) public void divisioniPer0() { Div.div(0, 0); } ...Adesso il test
divisioniPer0
non passa, visto che non viene
lanciata nessuna eccezione. Se nel corpo del metodo si invocasse
div(8, 0)
il test fallirebbe comunque, visto che si verifica
uno StackOverflowError
, in luogo di una ArithmeticException
.
Per fare in modo che il test passi bisogna modificare il codice di
div(...)
, in modo che lanci l'eccezione se il divisore è zero:
... /** divisione intera per sottrazioni successive */ public static int div(int a, int b) { if( b == 0 ) throw new ArithmeticException("/ by zero"); if( a == 0 || a < b ) return 0; ... } ...Le stesse considerazioni possono essere fatte per il metodo
mod(...)
.
È possibile indicare a JUnit che un test deve considerarsi fallito se trascorre un certo numero di millisecondi - ad esempio per un timeout di 30 secondi si può scrivere:
@Test (timeout = 30000) ...
JUnit permette di isolare il codice di setup, che deve essere eseguito
prima di ogni test della classe - ad esempio per istanziare un
oggetto - e il codice di teardown, eseguito dopo ogni test -
ad esempio per liberare risorse, chiudere file, eccetera.
Questo codice va messo in metodi annotati con @Before
e @After
, con nomi a piacere:
... @Before public void setUp() { ... } @After public void tearDown() { ... } ...Quindi, in una classe con due metodi di test,
test1
e
test2
, la sequenza di esecuzione sarà:
setUp test1 tearDown setUp test2 tearDownoppure con test1 e test2 scambiati.
Le classi di test possono essere messe nella stessa directory delle classi da testare, in modo che abbiano accesso ai membri protected. Una variazione può essere quella di mettere la classe di test nello stesso package della classe da testare, ma in una diversa directory.
Riprendendo l'esempio della divisione, dichiarando DivTest
come
facente parte del package algebra, si possono strutturare le directory così:
src/algebra/Div.java test/algebra/DivTest.javaIn questo modo i test sono ben separati dai sorgenti, pur appartenendo allo stesso package! Naturalmente quando si eseguono i test le directory
src/
e test/
- oltre a
junit-4.0.jar
- devono essere incluse nel classpath:
java -cp <path programma>/src:<path programma>/test:<path junit>/junit-4.0.jar org.junit.runner.JUnitCore algebra.DivTest
Molti ritengono che sia una buona pratica scrivere i test prima del codice, in modo che la scrittura del codice sia diretta verso la riuscita dei test. Questo implica che i test siano buoni, ovvero che verifichino anche situazioni particolari, come la divisione per 0 nel nostro esempio. Per questo l'impegno nel preparare i test dovrebbe essere pari a quello speso nello scrivere il codice da testare.