Dopo aver visto i fondamenti di utilizzo di JUnit, ne approfondiamo la conoscenza in questa lezione prendendo confidenza con un'altra libreria che permette di aumentare notevolmente la leggibilità del codice di collaudo: Hamcrest.
Utilizzare Hamcrest
L'esempio che vedremo in questa lezione utilizza la libreria Hamcrest. Per usarla, dobbiamo innanzitutto gestire le dipendenze richieste, inseriamo le seguenti all'interno del file build.gradle del modulo applicativo:
dependencies
{
...
...
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3'
...
...
}
Abbiamo incontrato la prima anche nella lezione precedente e, come abbiamo visto, Android Studio la inserisce di default nei nuovi progetti. La seconda è la libreria Hamcrest. Questa fornirà dei metodi, detti matcher, distribuiti in varie classi, che implementeranno delle regole di confronto finalizzate ad individuare coincidenze tra oggetti o loro proprietà. Questi matcher saranno usati in combinazione con gli assert
di JUnit. A tal proposito, si consideri che JUnit include già il core della libreria Hamcrest, e ce ne potremmo accontentare. Tuttavia, la dipendenza aggiuntiva che abbiamo inserito offre la possibilità di utilizzare diverse funzionalità aggiuntive.
Nell'esempio di questa lezione sottoporremo a collaudo ancora una volta la classe DataManager, che permette di inserire dati personali di individui all'interno di un'app Android, gestendoli tramite un oggetto ArrayList
. Alcuni dei metodi che ne faranno parte ricalcheranno quelli visti nella lezione precedente:
public class DataManager {
private ArrayList<Persona> elenco=null;
DataManager()
{
elenco=new ArrayList<Persona>();
}
void nuovoInserimento(Persona nuovo)
{
elenco.add(nuovo);
}
Persona recuperaPersona(int i)
{
return elenco.get(i);
}
int numeroInserimenti()
{
return elenco.size();
}
void cancellaPersona(int pos)
{
if (numeroInserimenti()>0 && pos<elenco.size())
elenco.remove(pos);
}
List<Persona> trovaPiuAnziani()
{
if (elenco.size()==0)
return null;
ArrayList<Persona> piuAnziani=new ArrayList<Persona>();
piuAnziani.add(elenco.get(0));
for(int i=1; i<elenco.size(); i++)
{
Persona p=elenco.get(i);
if (p.getEta()>piuAnziani.get(0).getEta())
{
piuAnziani.clear();
piuAnziani.add(p);
}
else if (p.getEta()==piuAnziani.get(0).getEta())
piuAnziani.add(p);
}
return piuAnziani;
}
}
Oltre ai primi metodi, di normale manipolazione della struttura dati, l'ultimo implementa un piccolo algoritmo per recuperare la lista dei soggetti più anziani tra quelli inseriti. Le specifiche di funzionamento che richiederemo a questo metodo sono:
- se l'elenco di persone è ancora vuoto, esso deve restituire
null
; - se è stato inserito un solo soggetto - che ovviamente sarà il più anziano - verrà collocato comunque in un'ArrayList;
- se sono stati già inseriti i dati di almeno due individui, sarà restituita una lista nuova contenente le persone che hanno l'età maggiore.
In pratica, siamo alla ricerca non dell'efficienza dell'algoritmo, quanto della sua correttezza in ogni caso di esecuzione.
Utilizzeremo un'annotazione di cui non ci siamo serviti nella scorsa lezione, @Before
, che introduce un metodo da eseguire prima di ogni test: il suo scopo principale sarà quello di introdurre delle inizializzazioni in modo da permettere una partenza "pulita" del test successivo.
public class DataManager {
private DataManager dm;
@Before
public void setUp() {
dm = new DataManager();
}
// i metodi di test da eseguire
}
Il metodo setUp
sarà eseguito prima di ogni test che vedremo e fornirà, di volta in volta, un oggetto DataManager
appena creato. Si noti a tal proposito che un'istanza della classe DataManager
deve essere membro della classe che contiene i test. Analogamente, esiste l'annotation @After
per poter effettuare operazione di pulizia e chiusura di eventuali collegamenti a risorse esterne.
Il primo test che inseriremo è un remake di un metodo eseguito nella lezione precedente: faremo la medesima prova sfruttando però gli strumenti di cui stiamo trattando.
@Test
public void test_inserimento() {
dm.nuovoInserimento(new Persona("Giulio", "Rossi", 34, true));
dm.nuovoInserimento(new Persona("Paolo", "Verdi", 25, false));
dm.nuovoInserimento(new Persona("Silvio", "Bianchi", 63, true));
assertThat(3, is(equalTo(dm.numeroInserimenti())));
}
Lo scopo è verificare che i tre inserimenti siano stati eseguiti in maniera corretta. Per il confronto utilizziamo il metodo assertThat
, che permette di costituire confronti più liberi tra il valore atteso e quello reale rispetto alle versioni specifiche che abbiamo già visto, come assertEquals
, assertTrue
, eccetera. In questo caso, completiamo il senso del confronto sfruttando i metodi is
e equalTo
offerti da Hamcrest. Il secondo specificherà che siamo alla ricerca di un'uguaglianza mentre il primo è una specie di adattatore che contribuisce per lo più alla leggibilità. Si noti che per utilizzarli con maggiore comodità - contribuendo anche alla leggibilità del codice - si può eseguire l'import statico dalla libreria di appartenenza di questi metodi e degli altri che useremo:
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
Altri due test che inseriamo iniziano a verificare il funzionamento del metodo trovaPiuAnziani della classe DataManager:
@Test
public void test_null() {
List<Persona> p=dm.trovaPiuAnziani();
assertThat(p, nullValue());
}
@Test
public void test_una_sola_persona() {
dm.nuovoInserimento(new Persona("Giulio", "Rossi", 63, true));
List<Persona> p=dm.trovaPiuAnziani();
assertThat(p.get(0), sameInstance(dm.recuperaPersona(0)));
}
Il primo test controlla che nel caso in cui non siano stati inseriti oggetti nell'ArrayList
, il metodo che cerca i più anziani restituisca un valore null
e non una lista vuota. Il metodo nullValue
è un altro importante elemento messo a disposizione da Hamcrest. Il secondo metodo verifica che nel caso in cui sia stato eseguito un solo oggetto, questo venga restituito direttamente. Il metodo sameInstance
controlla infine che gli oggetti confrontati siano esattamente gli stessi, ossia che facciamo riferimento alla stessa locazione di memoria.
L'ultimo test che inseriamo svolge l'accertamento più completo: la ricerca dei più anziani tra varie persone.
@Test
public void test_piu_persone() {
dm.nuovoInserimento(new Persona("Giulio", "Rossi", 63, true));
dm.nuovoInserimento(new Persona("Paolo", "Verdi", 25, false));
dm.nuovoInserimento(new Persona("Silvio", "Bianchi", 63, true));
dm.nuovoInserimento(new Persona("Paolo", "Verdi", 58, false));
List<Persona> p=dm.trovaPiuAnziani();
assertThat(p, hasSize(2));
}
Inseriamo quattro individui, di cui due hanno la stessa età e sono i più anziani. Entrambi dovranno essere restituiti nella lista, la cui dimensione dovrà essere pari a due. Il metodo hasSize
è un altro matcher che viene offerto da Hamcrest nel package collection, e che torna utile in casi come quello appena descritto.
Conclusioni
Oltre all'utilizzo di JUnit, in queste due lezioni abbiamo potuto appurare che per poter rendere "testabile" il nostro codice si deve partire da una corretta progettazione "ad oggetti", scindendo le responsabilità in classi separate: meno mescoleremo le funzionalità in metodi troppo aggregati, più sarà possibile svolgere test unitari. Hamcrest, dal canto suo, offrirà flessibilità ed espressività al codice di collaudo.
Se vogliamo consultare meglio i matcher offerti da Hamcrest, possiamo fare riferimento alla documentazione ufficiale.