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