Framework e strumenti per lo sviluppo di mock nell`ambito del

annuncio pubblicitario
Facoltà di Ingegneria
Corso di Studi in Ingegneria Informatica
Elaborato finale in Ingegneria del Software
Framework e strumenti per lo sviluppo di mock
nell'ambito del testing di applicazioni sviluppate in
linguaggio Java
Anno Accademico 2013/2014
Candidato:
MARIO ORIENTE
Matr.: N46001201
«Once,» said the Mock Turtle at last, with a deep sigh, «I was a real Turtle.»
(Alice In Wonderland, Lewis Carroll)
II
Indice
Introduzione
4
Capitolo 1: Teoria dei test
5
1.1
1.2
1.3
1.3.1
Definizioni
Livelli di Test
Modello Test-Driven Development
Vantaggi del TDD
Capitolo 2: Mock Objects
2.1
2.2
2.3
2.4
2.5
2.5.1
2.5.2
2.5.3
2.5.4
2.6
Conclusioni
Bibliografia
Introduzione
Unit Testing e Test Double
JUnit
Un esempio di mocking manuale
Framework disponibili
EasyMock
JMock
Mockito
JMockit
Tabella comparativa
5
6
7
9
10
10
11
15
19
22
23
24
26
27
29
30
31
III
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Introduzione
Nei prossimi paragrafi1 sono illustrate alcune tecniche di progettazione software basate sui
test e di come sia possibile migliorare la fase di sviluppo di applicazioni in ambiente Java.
Questo è possibile avvalendosi di librerie per la creazione e ripetizione di suite di test utilizzando i Mock Objects come strumento di design ancor prima che di testing.
Nel primo capitolo è spiegato cos’è il testing e perché è consigliabile basare lo sviluppo di
applicazioni, in questo caso in ambiente Java ma il discorso è analogo a prescindere
dall’ambiente utilizzato, a partire dai test più che dalle specifiche finali, tale approccio
prende il nome di Test-Driven Development in quanto guidato dai test.
Nel capitolo successivo sono introdotti i Mock Objects e, attraverso degli esempi, si cercherà di spiegare i vantaggi che offrono e come sia possibile migliorare tutta la fase di
sviluppo software.
1
Mi scuso per le innumerevoli occorrenze delle parole test e mock
4
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Capitolo 1
Teoria dei test
1.1 Definizioni
L’Institute of Electrical and Electronics Engineers2 definisce il Software Testing come:
Testing.
(1) «The process of operating a system or component under specified conditions, observing
or recording the results, and making an evaluation of some aspect of the system or component». (IEEE Std 610.12-1990)
(2) «The process of analyzing a software item to detect the differences between existing
and required conditions (that is, bugs) and to evaluate the features of the software items».
(IEEE Std 829-1983)
Test objective.
«An identified set of software features to be measured under specified conditions by comparing actual behavior with the required behavior described in the software documentation». (IEEE Std 1008-1987)
2
Sito ufficiale: http://www.ieee.org/
5
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
1.2 Livelli di test
Esistono diverse tipologie di test che si differenziano in base alla loro destinazione d’uso.
•
Test di unità: valuta la correttezza degli algoritmi;
•
Test di integrazione: valuta la correttezza delle interfacce;
•
Test di sistema: il software è confrontato con la specifica dei requisiti (verifica);
•
Test di accettazione: il software è confrontato con i requisiti dell’utente finale (validazione);
•
Test di regressione: verifica che non si siano introdotti difetti nelle versioni successive.
Secondo la tesi di Djikstra3, il test di un programma può rilevare la presenza di malfunzionamenti ma non dimostrarne l’assenza.
I test di unità consentono di scovare difetti nel codice nelle prime fasi di sviluppo.
Essi sono test dal punto di vista del programmatore e non dell'utente finale. I test funzionali, invece, dimostrando l'aderenza di una funzionalità alle specifiche, sono orientati verso il
punto di vista dell'utente finale.
Lo unit test permette di verificare piccoli moduli del software (unità) e dà la ragionevole
certezza che l'applicazione in generale possa funzionare bene una volta che si rilascia il
pacchetto vero e proprio. Un ulteriore distinzione possibile a questo livello è tra test strutturale e test funzionale.
Il test funzionale prevede il passaggio di un input e la restituzione di un output al nostro
software ma quello che succede in mezzo viene oscurato con il principio della “black box”.
Non sapremo mai se tutto quello che avviene in mezzo si comporta esattamente come dovrebbe, anche se apparentemente osserviamo i risultati attesi.
Il test strutturale, detto anche “white box”, è applicato alla struttura interna del programma,
il codice. Lo scopo è quello di eseguire ogni istruzione possibile massimizzando il fattore
di copertura per testare il maggior numero possibile di porzioni di codice. Questo criterio si
basa sull’analisi del flusso di controllo dei dati di un programma.
3
Edsger Wybe Dijkstra, http://it.wikipedia.org/wiki/Edsger_Dijkstra
6
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
1.3 Modello Test-Driven Development
Il temine Test-Driven Development (TDD) indica la metodologia di sviluppo software
"guidata dai test", tipicamente utilizzata con modelli Agili come l’eXtreme Progrmming4.
L’idea è quella di scrivere un test di un programma a partire da una determinata funzionalità prima di aver scritto il codice da testare. Con questo approccio il testing non è solo la fase in cui sono risolti i difetti per gli utenti finali, ma diventa parte fondamentale per lo sviluppo del codice applicativo e aiuta gli sviluppatori a capire di quali caratteristiche hanno
bisogno gli utenti e fornisce queste caratteristiche in modo affidabile.
Le fasi di sviluppo del codice secondo il metodo Test-Driven Development sono tre:
1. Red: Scrivere un test che obbligatoriamente fallisca;
2. Green: Apportare le necessarie modifiche al codice per superare il test;
3. Refactor: Eliminare dal codice eventuali ridondanze senza modificare il corretto
funzionamento.
Ripetere creando il test di una nuova funzionalità da implementare.
Figura 1 – TDD Mantra: Red, Green, Refactor [4]
4
Metodo Agile ideato da Kent Beck, Ward Cunningham e Ron Jeffries: http://xprogramming.com/
7
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Figura 2 – Ciclo di sviluppo del TDD
8
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
1.3.1 Vantaggi del TDD
•
Posso concentrarmi sullo sviluppo di piccole porzioni di codice che testerò subito;
•
Fasi di sviluppo piccole e incrementali;
•
In ogni momento dello sviluppo abbiamo la ragionevole certezza che la nostra applicazione funzioni correttamente grazie al superamento delle suite di test;
•
Sono sempre in grado di effettuare dei test di non regressione sul codice precedentemente realizzato;
•
Maggiore comprensione dei requisiti richiesti;
•
Significativa riduzione del tempo che lo sviluppatore dedica al debugging
dell’applicazione in cerca di errori;
•
Codice più modulare e ben progettato.
Un buon sviluppo in TDD si traduce in:
•
Sistema funzionante;
•
Non vi è duplicazione di codice;
•
Componenti sviluppati con il principio della visibilità minima;
•
Alta coesione e basso accoppiamento;
•
Basso numero di linee di codice per ogni metodo/funzione.
Esistono anche aspetti negativi nell’uso di questo approccio, ad esempio non sempre tutto è
testabile, in particolare quegli applicativi basati su molti elementi grafici e che hanno
un’alta interazione con l’utente sono scarsamente testabili.
Inoltre, il TDD ha una curva di apprendimento lenta, chi è nuovo di questa tecnica potrebbe non beneficiare – almeno all’inizio – di vantaggi in termini di velocità nello sviluppo
del codice in quanto si troverebbe con un approccio totalmente diverso da altre tecniche di
design, che prevedono la fase di testing solo alla fine dello sviluppo.
Infine, il programmatore tende a difendere il proprio lavoro a differenza di un tester esterno che non ha preso parte al progetto.
9
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Capitolo 2
Mock Objects
«You mock me, sir.»
(Hamlet, Act 5 Scene 2,
William Shakespeare)
2.1 Introduzione
I Mock Objects sono uno strumento di supporto al testing e quindi al Test-Driven Development, in alcuni casi sono un strumento di design vero e proprio, appartengono – assieme
agli stub, fake e dummy – alla famiglia dei Test Double [5] ma sono gli unici a permettere
verifiche di comportamento, oltre che di stato come tutti gli altri.
Questo consente non solo di effettuare un controllo sugli eventuali valori restituiti da un
metodo, ma anche di stabilire se sono stati invocati i metodi corretti nel modo atteso.
I Mock sono utilizzati per eseguire test di componenti che per funzionare necessitano di interfacciarsi con altre componenti non disponibili nella fase di test magari perché non ancora implementate o perché non accessibili come ad esempio un database esterno o una banca
per transazioni monetarie. Esternamente, sono indistinguibili dagli oggetti reali che sostituiscono, presentano le stesse API e offrono servizi che consentono di testare meglio un sistema basandosi sul comportamento atteso delle sue componenti sotto test.
10
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.2 Unit Testing e Test double
Immaginiamo uno scenario del genere:
cuoco <-­‐ cameriere <-­‐ cliente Per eseguire dei test di unità di questo sistema, partendo dalla componente di più basso livello come il cuoco, usiamo questo schema:
cuoco <-­‐ test driver Il test driver, semplicemente, ordina una serie di portate e verifica che la classe cuoco restituisca la corretta pietanza per ogni ordine effettuato.
Diversa è la situazione se si vuole testare un componente intermedio, come la classe cameriere.
Volendo usare lo stesso approccio usato con la classe cuoco, avremmo:
cuoco <-­‐ cameriere <-­‐ test driver Il test driver, analogamente al caso precedente, effettua differenti ordinazioni e si assicura
che il cameriere porti il piatto atteso.
Anzitutto, la classe cameriere sotto test è influenzata dal funzionamento della classe cuoco;
inoltre, vi è una maggiore dipendenza se sono presenti comportamenti non deterministici
come ad esempio la portata “piatto dello chef”.
Eseguire test di unità significa testare ogni componente in maniera indipendente, quindi un
migliore approccio per isolare la classe sotto test è usando dei Test Double, modificando il
sistema come segue:
-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐ | | v | cuoco test <-­‐ cameriere <-­‐ test driver 11
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Cuoco test può essere implementato in diversi modi:
•
cuoco fake – qualcuno che finge di essere un cuoco utilizzando cene surgelate e un
forno a microonde;
•
cuoco stub – un cuoco capace di preparare solo la lasagna e la prepara a prescindere
dall’ordine ricevuto;
•
cuoco mock – un cuoco sotto copertura che, seguendo precise indicazioni, simula in
tutto il comportamento del vero cuoco.
Volendo focalizzarci sull’utilizzo dei Mock, il nostro sistema sotto test diventa:
-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐ | | v | cuoco mock <-­‐ cameriere <-­‐ test driver Con questo approccio, l’oggetto Mock conosce in anticipo quale ordine sarà effettuato ed è
in grado di verificare se un comportamento è un evento atteso oppure no.
Un caso di test potrebbe essere il seguente:
•
Cuoco mock riceve in anticipo dal test driver indicazioni sull’ordine: una lasagna
•
Il test driver (fungendo da cliente) ordina una lasagna al cameriere
•
Il cameriere inoltra la richiesta di preparare una lasagna al cuoco
•
Il cameriere serve al cliente (test driver) la lasagna richiesta
TEST SUPERATO
Un altro caso di test potrebbe essere:
•
Cuoco mock riceve in anticipo dal test driver indicazioni sull’ordine: una lasagna
•
Il test driver (fungendo da cliente) ordina una lasagna al cameriere
•
Il cameriere inoltra la richiesta di preparare i cannelloni al cuoco
•
Il cuoco mock interrompe il test perché si aspettava una richiesta per una lasagna
•
Il test driver riconosce l’errore: il cameriere ha cambiato l’ordine
TEST FALLITO
12
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Un terzo caso di test:
•
Cuoco mock riceve in anticipo dal test driver indicazioni sull’ordine: una lasagna
•
Il test driver (fungendo da cliente) ordina una lasagna al cameriere
•
Il cameriere inoltra la richiesta di preparare una lasagna al cuoco
•
Il cameriere serve al cliente (test driver) un piatto di gnocchi
•
Il test driver riscontra una portata non attesa: il cameriere ha portato l’ordine sbagliato
TEST FALLITO
In generale, l’utilizzo dei Mock Objects è preferibile in quanto ci consente:
•
Di isolare un singolo metodo di una classe sotto test indipendentemente da altri metodi eventualmente invocati, rendendo i test di unità realmente possibili in senso
stretto;
•
Di diminuire i tempi di sviluppo, in quanto è più veloce istanziare un Mock Object
che scrivere un’intera classe che renda possibile il test;
•
Di eseguire il test senza bisogno di interfacciarsi con risorse esterne come l’accesso
a un database, richiedere un servizio web, leggere un file su disco, mandare
un’email, effettuare addebiti su carte di credito e così via.
Un particolare vantaggio, inoltre, si ha quando il loro utilizzo è associato al design pattern
Dependency Injection: «is a design pattern that shifts the responsibility of resolving dependencies to a dedicated dependency injector that knows which dependent objects to
inject into application code». [6]
13
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Con l’utilizzo dei Mock Objects è possibile creare un ambiente controllato con comportamenti deterministici e programmati, atti a facilitare o, addirittura, a rendere possibili test di
unità che altrimenti sarebbero quantomeno dispendiosi da realizzare.
In figura 3 è rappresentato uno schema di quello che può essere un qualsiasi sistema reale
con evidenziata in verde l’unità sotto test e in giallo le sue dipendenze dirette. In basso, invece, l’unità sotto test con le dipendenze mockate.
Figura 3 – In alto il sistema reale, in basso l’unità sotto test che si interfaccia ai Mock Objects creati
14
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.3 JUnit
JUnit5 è il framework per la creazione di test in ambiente Java più diffuso e utilizzato.
Come suggerisce il nome, esso è nato per facilitare la creazione di unit test.
Per il suo corretto utilizzo esistono una serie di regole da seguire, alcune non più necessarie
a partire dalla versione 4.0 (al momento, l’ultima release stabile è la 4.11) ma che è buona
norma seguire per facilitare la lettura del codice di test:
•
La pratica comune suggerisce di creare una classe di test JUnit associata alla classe
sotto esame. Al suo interno è possibile creare tanti metodi quanti sono i casi che si
vogliono verificare (tipicamente almeno uno per ogni metodo pubblico).
•
È possibile definire una “suite” che raggruppa più test JUnit. In questo modo è possibile eseguire tutti casi definiti in tutte le classi della suite in una sola volta.
•
JUnit suggerisce che per ogni classe da testare, venga creata una classe con lo stesso nome a cui aggiungere il suffisso “Test”. In questo modo vi è una chiara distinzione tra qual è il codice operativo e quale ne verifica il funzionamento, mantenendo al contempo evidente la loro relazione.
Tutto questo, assieme alle convenzioni di scrittura suggeriti da JUnit, permette di mantenere una struttura coerente anche tra sviluppatori diversi.
Inoltre, sempre a partire dalla versione 4.0, è possibile utilizzare una serie di annotazioni
utili a specificare come trattare il metodo annotato.
5
JUnit è incluso nella maggior parte degli ambienti di sviluppo Java. Sito web del progetto: http://junit.org/
15
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
@Test :
JUnit tratterà il metodo
public void
così annotato come un test case. Qualsiasi
eccezione derivante la sue esecuzione sancirà il fallimento del test. Al contrario, il test avrà
successo se nessuna eccezione sarà lanciata. Questa annotazione consente l’utilizzo di due
parametri facoltativi: excepted e timeout.
Con il primo si può specificare che tipo di eccezione aspettarsi dal test e nel caso venga
lanciata un’eccezione diversa da quella specificata o nessuna eccezione, il test fallisce.
Il secondo parametro serve a stabilire un tempo massimo (espresso in millisecondi) entro il
quale il test deve concludersi, superato il tempo indicato il test fallisce;
@BeforeClass:
Il metodo indicato con questa annotazione sarà eseguito una sola volta
prima di ogni altro metodo nella classe di test, serve nel caso in cui bisogna rendere disponibili delle risorse esterne per la corretta esecuzione dei test (ad esempio il login ad un database);
@AfterClass:
Dopo l’esecuzione di tutti i test nella classe, il metodo annotato con questa
dicitura sarà eseguito per liberare tutte le risorse inizializzate in @BeforeClass.
@Before:
Prima dell’esecuzione di ogni test i metodi contrassegnati con questa annota-
zione sono eseguiti per istanziare tutti gli oggetti necessari all’esecuzione dei singoli test
case.
@After:
Per liberare eventuali risorse istanziate con i metodi annotati con
@Before
eseguito questo metodo alla fine di tutti i test case anche se i metodi annotati con
e
@Test
sarà
@Before
hanno lanciato delle eccezioni.
@Ignore:
A volte può essere utile escludere dall’esecuzione uno specifico metodo @Test o
un’intera classe di test, ad esempio se non è stato ancora implementato il codice necessario.
Questa annotazione accetta come parametro una stringa che può motivare la scelta di ignorare uno specifico test.
16
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Di seguito un esempio di codice con le annotazioni sopra descritte:
import org.junit.*;
@BeforeClass
public static void initGlobalResources() {
// inizializzazione eseguita solo una volta, prima di ogni altra cosa
}
@Before
public void setUp() {
// inizializzazione eseguita prima di ogni test
}
@Test(expected=Exception.class)
public void testCase1() {
// esecuzione di un test con una specifica eccezione attesa
}
@Test(timeout=100)
public void testCase2() {
// esecuzione di un test con un tempo massimo di 100ms
}
@Ignore @Test
public void testCase3() {
// questo test non sarà eseguito
}
@After
public void tearDown() {
// pulizia eseguita dopo ogni test
}
@AfterClass
public static void closeGlobalResources () {
// eventuale pulizia di risorse istanziate con @BeforeClass
// eseguita una sola volta, al termine di tutti i test case
}
L’esito di ogni singolo test è valutato in base a delle asserzioni.
L’asserzione può essere:
•
vera: il test è andato a buon fine;
•
falsa: il test è fallito, evidenziando un comportamento non atteso del codice.
17
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Un elenco esauriente ma non esaustivo delle asserzioni disponibili è il seguente:
•
assertTrue(boolean condition)
asserisce che la condizione è vera, se non lo è lancia un
•
assertFalse(boolean condition)
asserisce che la condizione è falsa, se non lo è lancia un
•
AssertionError
AssertionError
assertEquals(java.lang.Object expected, java.lang.Object actual)
asserisce che gli oggetti sono uguali, se non lo sono lancia un
•
assertNull(java.lang.Object object)
asserisce che l’oggetto è null, se non lo è lancia un
•
AssertionError
assertNotNull(java.lang.Object object)
asserisce che l’oggetto non è
•
AssertError
null,
se lo è lancia un
AssertionError
assertSame(java.lang.Object expected, java.lang.Object actual)
asserisce che due oggetti fanno riferimento allo stesso oggetto (non basta che siano
uguali), in caso contrario lancia un AssertionError
•
assertNotSame(java.lang.Object unexpected, java.lang.Object actual)
asserisce che due oggetti fanno riferimento a oggetti diversi, in caso contrario lancia un AssertionError
•
fail(java.lang.String message)
interrompe il test con il messaggio dato
Tutti le asserzioni sopra elencate accettano un primo parametro opzionale che permette di personalizzare il messaggio di errore restituito:
java.lang.String message
18
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.4 Un esempio di mocking manuale
Immaginiamo di voler realizzare una classe che gestisce l’invio di fatture ai clienti. Alcuni
clienti preferiranno ricevere la fattura via email, altri su carta stampata. Di seguito un semplice esempio di implementazione:
public class Fatturazione {
private Stampante stampante = null;
private Email email = null;
public Fatturazione(Stampante stampante, Email email) {
this.stampante = stampante;
this.email = email;
}
public void preparaFattura(Fattura fattura, Cliente cliente) {
if(cliente.preferisceEmail()) {
email.inviaFattura(fattura, cliente.getEmail());
}
else {
stampante.stampaFattura(fattura);
}
}
}
Di seguito, invece, il codice necessario per testare la classe Fatturazione:
public class
private
private
private
FatturazioneTest {
Fatturazione fatturazione = null;
Cliente cliente = null;
Fattura fattura = null;
@Before
public void beforeEachTest() {
cliente = new Cliente();
fattura = new Fattura();
fatturazione = new Fatturazione(ServzioStampante(), ServizioEmail());
}
@Test
public void clienteConFatturaDigitale() {
cliente.fatturaDigitale(true);
fatturazione.preparaFattura(fattura, cliente);
}
@Test
public void clienteConFatturaStampata() {
cliente.fatturaDigitale(false);
fatturazione.preparaFattura(fattura, cliente);
}
}
19
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Eseguiamo il nostro codice e osserviamo che tutto funziona come previsto, bene.
Tuttavia, per l’esecuzione di questo test di unità, abbiamo:
•
inviato una email ad un cliente che non esiste (poco male)
•
lanciato una stampa sulla stampante della società (molto male)
•
evitato l’utilizzo di qualsiasi asserzione (malissimo)
Possiamo scrivere delle classi fittizie da usare solo per l’esecuzione dei test ed evitare così
effetti collaterali che avremmo se utilizzassimo le classi reali come visto in precedenza.
public class StampanteMock
implements Stampante {
boolean stampaEffettuata = false;
@Override
public void stampaFattura(Fattura fattura) {
stampaEffettuata = true;
}
public boolean stampaEffettuata() {
return stampaEffettuata;
}
}
public class EmailMock
implements Email {
boolean emailInviata = false;
@Override
public void inviaFattura(Fattura fattura) {
emailInviata = true;
}
public boolean emailInviata() {
return emailInviata;
}
}
Entrambe le classi mockate simulano rispettivamente il processo di stampa e quello di invio email senza effettuarli realmente, registrando solo le richieste ricevute dall’esterno e
restituendo un valore booleano come esito dell’operazione.
Possiamo quindi riscrivere la nostra classe di test utilizzando le classi fittizie:
20
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
public class FatturazioneTest {
private
private
private
private
private
Fatturazione fatturazione = null;
Cliente cliente = null;
Fattura fattura = null;
StampanteMock stampanteMock = null;
EmailMock emailMock = null;
@Before
public void beforeEachTest() {
stampanteMock = new StampanteMock();
cliente = new Cliente();
fattura = new Fattura();
fatturazione = new Fatturazione(stampanteMock, emailMock);
}
@Test
public void clienteConFatturaDigitale() {
cliente.fatturaDigitale(true);
fatturazione.preparaFattura(fattura, cliente);
assertTrue(emailMock.emailInviata());
}
@Test
public void clienteConFatturaStampata() {
cliente.fatturaDigitale(false);
fatturazione.preparaFattura(fattura, cliente);
assertTrue(stampanteMock.stampaEffettuata());
}
}
Il mocking delle classi è servito per eseguire correttamente il test senza aver alcun impatto
sulle risorse usate, in quanto fittizie, questo agevola la ripetizione delle suite di test.
Esistono però degli svantaggi nel mocking manuale, ad esempio bisogna scrivere ogni classe da cui l’unità sotto test dipende senza dimenticare di aggiornarle nel caso in cui la classe
reale a cui fa riferimento subisce modifiche nel tempo.
Esistono dei framework che automatizzano questo processo di creazione a partire da classi
reali in maniera dinamica, ampliando le possibilità di utilizzo di questa tecnica e fornendo
una serie di vantaggi implementativi che di seguito andremo ad analizzare.
21
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.5 Framework disponibili
Abbiamo visto che i Mock Objects possono essere creati manualmente ma si rischia di introdurre difetti essendo codice scritto alla pari di quello sotto test. In pratica, esistono diverse librerie che ne facilitano l’utilizzo attraverso semplici chiamate di metodi.
Alcuni dei più diffusi framework attualmente disponibili sono:
•
EasyMock – www.easymock.org
•
JMock – www.jmock.org
•
Mockito – https://code.google.com/p/mockito/
•
JMockit – https://code.google.com/p/jmockit/
Per aggiungere un framework al proprio progetto Eclipse6 è possibile farlo dal menù contestuale del nostro Package, selezionando la voce Build Path e successivamente la voce Configure Build Path. Con il pulsante Add External JARs è possibile aggiungere una o più librerie desiderate. In figura 4 è mostrato un esempio di progetto a cui sono stati aggiunti i
file .jar necessari per l’utilizzo dei framework analizzati di seguito.
Nei prossimi paragrafi saranno descritte le principali caratteristiche e sarà presentato uno
pseudo codice di utilizzo per evidenziare le differenze sintattiche per ognuno di essi.
Figura 4 – Librerie esterne aggiunte al progetto Mocking
6
Ambiente di sviluppo integrato (IDE) multi-linguaggio e multipiattaforma, sito ufficiale: http://www.eclipse.org
22
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.5.1 EasyMock
EasyMock, giunto alla versione 3.2, mette a disposizione tre metodi per istanziare i Mock
Objects:
•
EasyMock.createMock():
non verifica l’ordine con cui i metodi sono chiamati e lan-
cia un’eccezione AssertionError per ogni metodo non atteso;
•
EasyMock.createStrictMock():
verifica l’ordine con cui i metodi sono chiamati e
lancia un’eccezione AssertionError per ogni metodo non atteso;
•
EasyMock.createNiceMock():
non verifica l’ordine con cui i metodi sono chiamati e
restituisce un valore tra 0, null e false per ogni metodo non atteso.
NiceMock
import
import
import
import
import
import
può essere usato come Stub, mentre Mock e StrictMock sono Mock puri.
static org.easymock.EasyMock.*;
org.easymock.EasyMockRunner;
org.easymock.TestSubject;
org.easymock.Mock;
org.junit.Test;
org.junit.runner.RunWith;
@RunWith(EasyMockRunner.class)
public class EasyMockTest {
@TestSubject
private ClassUnderTest classUnderTest = new ClassUnderTest();
@Mock
private Collaborator mock;
@Test
public void testEasyMock() {
replay(mock);
classUnderTest.someMethod("someParameter");
}
}
Questo framework:
•
Permette il mocking di interfacce, classi astratte e classi concrete;
•
Non permette il mocking di classi definite final, generando un’eccezione;
•
Non permette il mocking di metodi definiti static o final, utilizza i metodi reali se
invocati;
•
I valori di ritorno di default per i metodi mockati dipendono da quale metodo è stato usato per istanziarli.
23
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.5.2 JMock
JMock, al momento in versione 2.6.0, istanzia entrambi gli Stub e i Mock allo stesso modo,
può eseguire il mocking di interfacce attraverso la classe Mockery.
import
import
import
import
org.jmock.integration.junit4.JUnit4Mockery;
org.jmock.Mockery;
org.junit.Test;
org.junit.runner.RunWith;
@RunWith(JMock.class)
public class JMockTest {
//setUp
Mockery mockery = new JUnit4Mockery();
ClassUnderTest classUnderTest = mockery.mock(ClassUnderTest.class);
@Test
public void testJMock() {
classUnderTest.someMethod("someParameter");
}
}
Nativamente, con JMock non è possibile eseguire il mock di classi che non siano interfacce, è possibile ovviare questo limite attraverso l’utilizzo di ClassImposteriser:
import
import
import
import
org.jmock.integration.junit4.JUnit4Mockery;
org.jmock.Mockery;
org.junit.Test;
org.junit.runner.RunWith;
@RunWith(JMock.class)
public class ListTest {
Mockery context = new Mockery() {
setImposteriser(ClassImposteriser.INSTANCE);
}
@Test
public void shouldMockClass() {
ArrayList mockList = context.mock(ArrayList.class);
}
}
24
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Questo framework:
•
Permette il mocking di interfacce astratte e concrete;
•
Non permette il mocking di classi definite final, generando un’eccezione;
•
Non permette il mocking di metodi definiti static o final, utilizza i metodi reali se
invocati;
•
I valori di ritorno di default per i metodi mockati sono 0,
null , false,
empty
String, empty Array.
25
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.5.3 Mockito
Mockito non differenzia gli Stub dai Mock e per istanziarli entrambi utilizza il metodo
Mockito.mock().
La versione attualmente disponibile è la 1.9.5.
//Mock di un’interfaccia
List testDouble = Mockito.mock(List.class);
//Mock di una classe
ArrayList testDouble = Mockito.mock(ArrayList.class);
Consente, attraverso l’utilizzo di annotazioni, di indicare quali variabili devono essere
mockate:
import static org.mockito.Mockito.*;
public class MockTest extends TestCase {
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;
@Before public void setup() {
manager = new ArticleManager(userProvider, database, calculator);
}
}
public class SampleBaseTestCase {
@Before public void initMocks() {
MockitoAnnotations.initMocks(this);
}
}
Questo framework:
•
Permette il mocking di interfacce, classi astratte e classi concrete;
•
Non permette il mocking di classi definite final, generando un’eccezione;
•
Non permette il mocking di metodi definiti static o final, utilizza i metodi reali se
invocati;
•
I valori di ritorno di default per i metodi mockati sono 0, null , false .
26
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.5.4 JMockit
JMockit è il progetto più recente tra quelli presentati, nato nel 2009 è disponibile attualmente in versione 1.7, fornisce due modalità di utilizzo, la prima per il testing basato sul
comportamento (JMockit Expectations & Verificatoins) e la seconda per il testing basato
sullo stato (JMockit Annotations).
Illustriamo come si istanzia un Mock Objects per una verifica basata sul comportamento,
cioè quella di maggior interesse:
import
import
import
import
static mockit.Mockit.*;
junit.framework.TestCase;
mockit.*;
mockit.integration.junit4.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(JMockit.class)
public class MockTest extends TestCase {
@MockClass(realClass = LowerClass.class)
public static class LowerClassMock {
@Mock(invocations = 1)
public String doWork() {
return "something";
}
}
@Before
public void setUp() {
setUpMocks(LowerClassMock.class);
}
@After
public void tearDown() {
tearDownMocks();
}
@Test
public void testJMockit() {
ClassUnderTest classUnderTest = new ClassUnderTest();
classUnderTest.someMethod();
}
}
27
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
A differenza di tutti gli altri framework, JMockit è basato sul class remap: ogni oggetto di
una classe Mock creato sarà mockato anche se istanziato dalla classe originale.
Oltre il class remap, questo framework:
•
Permette il mocking anche di classi definite final;
•
Permette il mocking di metodi static,
•
Consente il mocking senza Dependency Injection (anche di istanze create con
private
e
final;
l’operatore new);
28
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
2.6 Tabella comparativa
Volendo riassumere le caratteristiche principali, per evidenziare le differenze tra i framework considerati, possiamo schematizzare i risultati riportati nella tabella 1.
Caratteristica
Versione attuale
Tipologia
File .jar unico in
classpath
EasyMock
JMock
Mockito
JMockit
3.2
2.6.0
1.9.5
1.7
11 Lug 2013
19 Dic 2012
6 Ott 2012
9 Mar 2014
Proxy-based
Proxy-based
Proxy-based
Class remap
√
√
√
√
√
√
√
√
√
√
√
Mock in cascata
@RunWith
non necessario
Mocking parziale
√
√
√
Auto injection dei Mock
Thread-safe
√
√
√
Mocking di classi final
√
Mock di enum
√
Injects dependencies
√
Sostituisce istanze create
con new
Eccezioni con messaggi
di errore personalizzati √
√
Tabella 1 – Confronto tra i framework considerati
29
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Conclusioni
L’utilizzo dei Mock Objects nella fase di testing agevola il processo di sviluppo consentendo di testare metodi e classi che altrimenti non potrebbero essere testati, non prima –
almeno – di aver completato l’intero progetto, rendendo disponibili tutte le componenti con
le quali il programma si andrà ad interfacciare.
Come abbiamo visto, il loro utilizzo è incoraggiato nell’approccio del Test-Driven Development basato sulla verifica di comportamento, tale approccio riduce considerevolmente i
tempi di sviluppo, genera di fatto solo codice che è stato già testato e quindi funzionante,
aiuta altresì a preservare l’incapsulamento e la non regressione. La scelta di utilizzare una
libreria piuttosto che un’altra si può considerare soggettiva, con il susseguirsi delle nuove
versioni ogni progetto ha implementato funzionalità peculiari introdotte inizialmente solo
da taluni framework, andando così a livellare i benefici offerti da tutte le librerie qui considerate.
Specifiche alla mano, probabilmente, la più completa da usare è JMockit che offre caratteristiche distintive non ancora disponibili in altri framework. La soggettività resta per quanto concerne la chiarezza e la semplicità – di utilizzo e di lettura – del codice implementativo necessario al loro corretto funzionamento.
Un altro fondamentale aspetto, da tenere in considerazione per la scelta di un framework, è
senza dubbio l’impegno che il team di sviluppo ha nell’aggiornare le librerie e
nell’implementare nuove funzionalità, oltre – perché no – alla community che si porta dietro. Sarà preferibile, quindi, scegliere un framework con aggiornamenti costanti nel tempo
che garantiscono la sempre attualità del progetto.
30
Framework e strumenti per lo sviluppo di mock nell'ambito
del testing di applicazioni sviluppate in linguaggio Java
Bibliografia
[1] Kent Beck, Test-Driven Development, By Example,
Addison-Wesley, 2002
[2] Martin Fowler, Mocks aren’t Stubs,
http://martinfowler.com/articles/mocksArentStubs.html, 2007
[3] Steve Freeman, Mock Roles, not Objects,
http://jmock.org/oopsla2004.pdf, 2004
[4] Steve Freeman, Pryce, Nat: Growing Object-Oriented Software, Guided by Tests,
Addison-Wesley, 2009
[5] Gerard Meszaros, xUnit Test Patterns, Refactoring Test Code,
Addison-Wesley, 2007
[6] Niko Schwarz, Mircea Lungu, Oscar Nierstrasz, Seuss: Decoupling responsibilities from
static methods for fine-grained configurability, Journal of Object Technology, Volume 11,
no. 1 (April 2012), pp. 3:1-23, http://dx.doi.org/10.5381/jot.2012.11.1.a3
31
Scarica