Strumenti di build e test con Groovy

IV parte: Groovy Testingdi

In quest‘ultimo articolo dedicato ai tools che Groovy offre allo sviluppo di applicazioni Java, vediamo in che modo Groovy può semplificare la fase di testing delle applicazioni, sia attraverso la sua sintassi lineare e concisa, perfetta per la stesura dei test, sia attraverso un supporto semplice e completo alla creazione di mock objects.

Introduzione

Pochi concetti hanno influenzato il processo di sviluppo del software negli ultimi anni come l'utilizzo di strumenti di testing automatici. Per molti sviluppatori oggi è quasi innaturale scrivere codice senza scrivere i relativi test, quando i test non sono scritti addirittura prima del codice di business, seguendo un approccio TDD (Test Driven Development). In ogni caso, sia che si segua uno sviluppo Test First che Test Last, la scrittura di test è diventata una parte molto importante nello sviluppo del software. Esistono vari tipi di test automatici, che si occupano di verificare varie parti di una applicazione: si possono quindi individuare Unit Test (per testare ogni singola parte del sistema in maniera indipendente dalle altre), Integration Test (per verificare la corretta interazione tra le varie componenti del sistema e con sistemi esterni, quali DB , filesystem...), Test Funzionali, Smoke Test, End-to-End Test, e cosi via.
In questo articolo ci concentreremo sugli Unit Test. Nel mondo Java esistono diversi framework che aiutano gli sviluppatori a scrivere in modo agevole i test e a eseguirli: i principali sono JUnit e TestNG, ai quali spesso si affiancano vari framework per la creazione di Mock Objects, quali EasyMock o jMock. Grazie a Groovy, è possibile scrivere test in modo più veloce e semplice, ed è possibile testare non solo applicazioni Groovy, ma anche applicazioni Java.
Groovy offre un supporto completo allo Unit Testing come parte integrante del linguaggio: il supporto a JUnit è predefinito nel linguaggio, rendendo cosi immediatamente disponibile il framework di test senza la necessità di aggiungere un'ulteriore dipendenza al progetto. Non solo, ma Groovy estende JUnit offrendo una classe di test "potenziata" con l'aggiunta di una nuova serie di asserzioni. Infine, Groovy offre direttamente un supporto semplice e potente per la creazione di mocks e stubs.

Groovy Testing Framework

In Groovy le asserzioni sono una parte integrante del linguaggio: questo significa che in qualunque punto nel codice è possibile scrivere istruzioni del tipo:

def number = 2
assert (2 + number) == 4

Ovviamente questo tipo di verifica molto semplice, se può essere utilizzata durante lo sviluppo per verificare semplici condizioni, non consente di eseguire dei veri e propri test sul codice sotto esame.

È quindi necessario scrivere una apposita classe di test. Groovy mette a disposizione una classe da estendere, GroovyTestCase, che a sua volta estende la classe TestCase di JUnit, alla quale aggiunge una serie di nuove asserzioni, tra le quali si possono menzionare assertToString, AssertArrayEquals e shouldFail.

Vediamo un primo esempio di test:

class SimpleUnitTest extends GroovyTestCase {   
    void testSimple() {
        def integerList = [1, 2, 3]
        assertEquals 2, integerList[1]
        assertEquals 6, integerList[0] + integerList[1] + integerList[2]
    }
}

Lanciando questo test (ad esempio attraverso la Groovy Console o da linea di comando con questa istruzione: groovy SimpleUnitTest.groovy) si ottiene il seguente output, familiare a chi ha già usato JUnit:

.
Time: 0.125
OK (1 test)

Attraverso l'asserzione shouldFail è possibile testare se una certa eccezione attesa viene effettivamente lanciata: ad esempio possiamo verificare che un metodo metodoA lanci una NumberFormatException con un input non numerico con questo test:

void testSimple() {
    def objectToTest = new ClassToTest()
    shouldFail(NumberFormatException) {
        objectToTest.metodoA('')
    }
}

È possibile eseguire i test anche lanciandoli attraverso un'IDE (ad esempio Eclipse, Netbeans o Idea), oppure attraverso script Ant, Gant, Maven o Gradle.

Costruire Mock Objects

Nel paragrafo precedente abbiamo visto come eseguire dei semplici Unit Test in Groovy; vediamo ora quali strumenti Groovy mette a disposizione per costruire Mock Objects. Prima è però utile definire alcuni concetti introduttivi su questa tecniche di testing. Prima di tutto vanno ricordate quelle che sono alcune caratteristiche principali di uno Unit Test: ripetibilità, automazione, isolamento e velocità. Proprio per rispettare questi requisiti è importante limitare le dipendenze della classe sotto test (CUT, Class Under Test). Un esempio tipico di dipendenza è dato dagli oggetti con cui la nostra CUT interagisce, oggetti che sono detti collaboratori della classe sotto test . Un altro caso molto comune di dipendenza si ha quando la nostra CUT utilizza sistemi esterni, come DataBase o Web Services. In presenza di dipendenze esterne, i risultati del nostro test potrebbero dipendere non solo dal codice sotto esame, ma anche dal risultato delle chiamate ai metodi dei collaboratori o a sistemi non direttamente controllabili. Proprio per poter fare test affidabili e ripetibili, è quindi utile far dipendere il codice che dobbiamo testare non dagli oggetti reali, ma da loro sostituti definiti direttamente durante il test e che danno risultati prevedibili. Questi "sostituti" prendono il nome di Mock o Stub. Nonostante spesso siano usati in modo quasi intercambiabile, questi due termini hanno in realtà un significato leggermente diverso.
Si definisce Stub un oggetto che si comporta come il vero collaboratore: il suo fine principale è quello di restituire il risultato atteso per l'esito positivo del test. Si dice che gli Stub hanno "aspettative deboli", definiscono cioè solamente lo stato del collaboratore. I Mock invece, oltre a restituire il risultato atteso, verificano anche come la CUT interagisce con i propri collaboratori, ad esempio controllando il numero di volte che un collaboratore viene chiamato o la sequenza delle chiamate. Si dice che i Mock hanno "aspettative forti", definiscono non solo lo stato dell'oggetto sostituito, ma anche il suo comportamento.

In Groovy ci sono diversi modi di realizzare oggetti Mock, alcuni che sfruttano direttamente la sua natura dinamica e quindi utilizzabili solo per testare altre classi Groovy, altri che possono essere usati anche per testare classi Java.

MockFor e StubFor

Le classi MockFor e StubFor si ispirano a Easymock, uno tra i più noti framework Java e possono essere usate per sostituire sia collaboratori scritti con Groovy che con Java; la CUT deve però essere scritta in Groovy. Queste classi permettono di definire il comportamento attraverso delle closure, consentendo cosi di restituire valori statici o calcolati, verificare i parametri ricevuti, lanciare eccezioni e cosi via.

Vediamo il loro uso con un esempio. Ipotizziamo di voler testare questa closure, che salva il contenuto di un testo in un file:

def save = {
    try {
        def file = new File('.', 'prova.txt')
        file.text = 'Qui va il contenuto del file'
        render "success"
    } catch (Exception e) {
        render "exception ${e.message}"
    }
}

Usiamo la classe StubFor per generare l'oggetto Mock:

void testSave() {
    def testObj = new ClassToTest()
    // Definisco la classe dell'oggetto Mock
    def fileMock = new StubFor(java.io.File)
    // Definisco i risultati attesi
    def resultText
    fileMock.demand.setText() { resultText = it }
    fileMock.demand.close {}
    // Utilizzo il mock
    fileMock.use {
        testObj.save()
    }
    assertEquals "Qui va il contenuto del file" , resultText
}

L'uso è il seguente: si definisce quale classe deve essere sostituita dallo stub, si definiscono i valori attesi e si richiama il metodo da testare all'interno della clausola 'use': le chiamate al metodo setText della classe File verranno sostituite con la chiamata al metodo definito nello stub. Possiamo anche notare un'altra cosa: nella nostra classe abbiamo dimenticato di chiudere il file, ma il nostro test passa lo stesso. Con l'uso dell'oggetto StubFor non è possibile testare questo caso: infatti con l'istruzione fileMock.demand.close{} noi definiamo solamente il valore restituito dal metodo close() (in questo caso nessun valore), ma non verifichiamo che il metodo sia effettivamente chiamato.

Per testare anche questo caso dobbiamo utilizzare la classe MockFor. Modifichiamo leggermente il codice da testare, chiamando due volte il metodo setText e aggiungendo una chiamata a close():

def save = {
    try {
        def file = new File('.', 'prova.txt')
        file.text = "Qui va il contenuto del file"
        file.text = "Qui aggiungo altro testo testo"
        file.close()
    } catch (Exception e) {
        log "exception ${e.message}"
    }
}

Il relativo codice di test è il seguente:

void testSave() {
    def testObj = new ClassToTest()
    def fileMock = new MockFor(java.io.File)
    def resultText1, resultText2
    fileMock.demand.setText() { resultText1 = it }
    fileMock.demand.setText() { resultText2 = it }
    fileMock.demand.close(1..1) {}
    fileMock.use {
        testObj.saveForMock()
    }
    assertEquals "Qui va il contenuto del file" , resultText1
    assertEquals "Qui aggiungo altro testo" , resultText2
}

Vediamo che possiamo definire con precisione il numero di chiamate ai metodi, il loro ordine e definire risultati diversi (le due chiamate ai metodi setText). Possiamo poi usare la usare la notazione (min..max) per definire il numero minimo e massimo di chiamate previste (in questo caso esattamente una chiamata al metodo close()).

Expando e mappe

Nel caso in cui l'oggetto da sostituire non sia creato internamente al metodo da testare, ma sia passato come parametro è possibile utilizzare altre tecniche, come gli Expando o le mappe.
Un Expando è un tipo di classe particolare alla quale è possibile aggiungere proprietà e metodi dinamicamente. È possibile sfruttare questa caratteristica per simulare il comportamento dell'oggetto da sostituire.

Ad esempio, un metodo che scrive un testo su un file (passato come parametro) può essere testato in questo modo:

def saveFile(file) {
    file.write "Testo del file."
}
void testSaveFile() {
    def fileMock
             = new Expando(text: '', write: {text = "Testo dinamico dell'Expando" })
    def testObj = new ClassToTest()
    testObj.saveFile(fileMock)
    assertEquals "Testo dinamico dell'Expando" , fileMock.text
}

È possibile anche definire un oggetto mock attraverso una mappa avente come chiave il nome del metodo da sostituire e come valore una closure contenente l'implementazione. Vediamo come testare il metodo saveFile usando questa tecnica:

void testSaveFile() {
    def text = ''
    def fileMock = [write : { text = "Testo dinamico della mappa" }]
    def testObj = new ClassToTest()
    testObj.saveFile(fileMock)
    assertEquals "Testo dinamico della mappa" , text    
}

In Groovy è possibile implementare una interfaccia utilizzando una mappa, e questo rende possibile utilizzare questo metodo per creare mock anche di classi Java.

Conclusioni

In questo articolo abbiamo visto alcune tecniche per testare sia classi Java che Groovy. Oltre a queste, è possibile creare mock objects di classi Groovy anche con altre tecniche, ad esempio usando le Categories, le ExpandoMetaClass, definendo delle closure o attraverso overriding dei metodi dei collaboratori.
L'obiettivo non era quello di creare un tutorial dettagliato ed esaustivo sul testing con Groovy, ma piuttosto di offrire una panoramica dei principali strumenti presentati dal linguaggio. In ogni caso, è importante non sottovalutare l'importanza di scrivere una buona suite di test, sopratutto se si utilizza un linguaggio dinamico. Abbiamo visto che utilizzando Groovy è possibile inoltre testare anche codice Java sfruttando la sua sintassi semplificata e la possibilità di generare in certe condizioni mock objects senza utilizzare framework esterni. Naturalmente, vista la stretta integrazione tra Groovy e Java è possibile anche utilizzare il proprio framework preferito per definire mock e aspettative.

Prima di concludere è utile accennare ad una nuova generazione di framework di test che stanno iniziando a diffondersi e che, utilizzando Groovy, permettono di definire dei casi di test con un linguaggio molto più ad alto livello; tra questi meritano di essere ricordati easyb e Spock, dei quali parleremo magari in futuro.

 

Riferimenti

[1] Groovy Testing Guide
http://groovy.codehaus.org/Testing+Guide

[2] D. Konig, A.Glover, P. King, G. Laforge, J. Skeet: Groovy in Action, Manning, 2006

[3] JUnit Home Page
http://www.junit.org/

[4] Venkat Subramaniam, "Programming Groovy. Dynamic productivity for the Java developer", The Pragmatic Programmer

[5] Martin Fowler - Mocks aren't Stubs
http://martinfowler.com/articles/mocksArentStubs.html

[6] Easyb
http://www.easyb.org/

[7] Spock framework
http://code.google.com/p/spock/

[8] Easymock
http://easymock.org/

 

 

Condividi

Pubblicato nel numero
143 settembre 2009
Davide Rossi si è laureato in Ingegneria informatica Presso il Politecnico di Milano. Si occupa di tecnologie Java dal 2003 e ha partecipato come consulente a vari progetti in ambito finanziario, marketing e di eCommerce.
Articoli nella stessa serie
Ti potrebbe interessare anche