CodingJam

Integration Test con Maven e Docker

Non è la prima volta che parliamo di integration test, croce e delizia dello sviluppo. Sono complessi da fare perché richiedono la compartecipazione dei sistemi necessari all’applicazione che siamo sviluppando. Avevamo visto come nel ciclo di build di Maven l’uso del plugin failsafe ci aiuta ad eseguire questo tipo di test nella fase giusta. Abbiamo visto poi come Arquillian permette di controllare un application server durante gli integration test. Quello che non abbiamo visto fino adesso è come controllare i sistemi a contorno, per esempio un database, per costruire dei veri e propri test automatici end-to-end. In questo post vedremo quindi come eseguire dei test di integrazione di una applicazione basata su Tomcat e PostgreSQL con l’aiuto di Docker!

Scenario

Immaginiamo di dover sviluppare i test di integrazione su una applicazione web, basata su Tomcat, che esegue il CRUD dei dati su un database (PostgreSQL in questo caso), attraverso una API REST (in questo caso possiamo definirli anche end-to-end, visto che no abbiamo interfaccia grafica). Il test più efficace è sicuramente quello più vicino ad una situazione reale: chiamate HTTP dai nostri test alla nostra applicazione deployata su un Tomcat che fa le query sul database.

Gli obiettivi sono:

Maven e Tomcat

Il primo obiettivo è facilmente raggiungibile con Maven: nel suo ciclo di build, prevede test e integration test. Ogni fase solitamente espone degli “hook” di pre e post fase, al quale si possono agganciare altri plugin per eseguire certe operazioni.

Possiamo così ottenere parzialmente il secondo e il terzo obiettivo con il tomcat7-maven-plugin, come avevamo già visto. Tomcat infatti viene scaricato al momento se non disponibile, rendendo il test portabile e indipendente dall’ambiente. Inoltre, è possibile avviare il server prima dei test e stopparlo al termine.

Ci sono però diversi vincoli:

E il database? Se si presuppone che il database sia sempre disponibile, possiamo accontentarci di questa soluzione (avendo presente i vincoli). Esiste un postgresql-maven-plugin molto interessante su Github, che promette di poter controllare PostgreSQL durante la fase di integration test. A giudicare dal codice sembra però che il database debba comunque essere installato sulla macchina: viene quindi meno la portabilità.

Per raggiungere pienamente i tre obiettivi sarebbe necessario che Tomcat e PostgreSQL girassero in contenitori isolati (per evitare conflitti di porte), consistenti e possibilmente scaricabili e configurabili in modo automatico. Per far questo, accanto a Maven abbiamo bisogno di Docker!

Docker Docker Docker!!

Il movimento dei DevOps ha avvicinato il mondo degli sviluppatori (Dev) a quello degli operatori (Ops), che, contaminandosi a vicenda, ha spinto l’applicazione del principio DRY (Don’t Repeat Yourself) a livello di gestione dell’infrastruttura e della delivery. Sono nati quindi strumenti fantastici come Vagrant per il provisioning delle macchine virtuali, nonché modalità di configurazione avanzate automatiche con Ansible, Puppet o Chef. L’idea è quella di creare artefatti immutabili, dove l’artefatto è una macchina virtuale!! C’è da modificare la configurazione della macchina? Bene, si aggiorna lo script e si rigenera la macchina in automatico, tutto in pochi minuti.

Gestire una infrastruttura virtualizzata con strumenti automatici permette di controllare cosa c’è installato sulle macchine (perché nessuno le modifica più) e soprattutto permette di replicarle senza nessuno sforzo. Di fatto però, è probabile che gran parte delle macchine virtuali abbiano in comune tutto lo strato del sistema operativo (Linux), che diventa quindi ridondante: perché quindi non “virtualizzare” solo una parte di esso, cioè quello che cambia tra una macchina e l’altra? Da questa idea nasce Docker!

Docker è quindi figlio della virtualizzazione, che non sostituisce: sfruttando caratteristiche specifiche del Kernel Linux (come cgroups introdotto da Google nel 2008), Docker crea dei contenitori isolati, visti dal sistema host come processi, che invece internamente appaiono come delle vere e proprie macchine virtuali con la propria interfaccia di rete e le proprie risorse (proprio grazie al partizionamento che ne fa cgroups). Già con questa premessa si capisce che sia i container Docker che la macchina host possono essere solo Linux. Recentemente sono usciti Docker per Mac e per Windows, che, invece di sfruttare VirtualBox per far girare una macchina virtuale Linux che faccia da host per Docker (come faceva fino a ieri Docker Toolbox), non fanno altro che sfruttare i virtualizzatori nativi introdotti nelle versioni più recenti di questi OS (rispettivamente xhyve e Hyper-V) per avviare Alpine Linux, una versione estremamente leggera di Linux adatta ad essere host di container, trasparente però ai sistemi operativi veri e propri ospitanti. Il risultato è quindi che si usa Docker da Mac o Windows come se si fosse su Linux.

Cosa se ne fa uno sviluppatore?

Docker può essere estremamente utile sia in fase di sviluppo che di test perché si possono creare, configurare, avviare e distruggere container in modo estremamente semplice. Inoltre, un container può essere usato anche come artefatto finale per il deploy!! Non è così più necessario preoccuparsi della configurazione dell’ambiente di produzione perché viene generato direttamente in fase di sviluppo: si parla così oggi di immutable deploy, dove ad ogni rilascio viene generata un nuova immagine Docker con la nostra applicazione. A differenza delle VM infatti, con Docker è buona norma creare contenitori con uno e un solo servizio, che si presta bene a contenere l’applicazione che stiamo sviluppando, in modo da essere facilmente replicabile (per scalare orizzontalmente) isolandolo dal sistema host.

Maven e Docker

Dopo aver tessuto le lodi di Docker e le modalità di utilizzo, vediamo come possono semplificarci la vita nei test di integrazione.

Riuscire a costruire dei test completamente automatici per la fase di “integration-test” di Maven che fossero i più vicini possibile al caso reale e che girassero anche sul server di Continuous Integration (come Jenkins) senza problemi è sempre stata una sfida. Adesso con Docker, controllato da Maven, è finalmente possibile in modo piuttosto semplice: il tempo impiegato inizialmente per “incastrare” le configurazioni è ampiamente ripagato dai risultati!

Prerequisiti:

Cercando “maven docker” su Google, spicca il plugin Maven di Fabric8 per Docker, soprattutto per l’ambia documentazione. In realtà esistono quattro plugin che permettono di usare Maven con Docker e quelli di Fabric8 li hanno messi a confronto. Indovinate chi vince?

Configurazione base con Fabric8 plugin

Prima di pensare a qualsiasi container da creare e avviare, è necessario innanzitutto impostare il plugin failsafe come abbiamo già imparato e poi il plugin di Fabric8 in modo che crei e/o avvii i container in fase di pre-integration-test e li fermi (e li distrugga) in fase di post-integration-test

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.15.16</version>
    <configuration>
        <showLogs>true</showLogs>
        ...
    </configuration>
    <executions>
        <execution>
            <id>start</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>build</goal>
                <goal>start</goal>
            </goals>
        </execution>
        <execution>
            <id>stop</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Questo è boilerplate: la parte più interessante invece sta nel blocco configuration.

PostgreSQL e Docker: test business logic su base dati

Cominciamo quindi con le cose semplici: avviare un container con PostgreSQL, creare il database e impostare username e password.

L’immagine Docker di PostgreSQL è già pronta per noi su Docker Hub!

Basta quindi aggiungere nel blocco configuration la configurazione per l’immagine Docker che ci interessa:

<images>
    <image>
        <alias>postgres-integration-test</alias>
        <name>postgres:9</name>
        <run>
            <env>
                <POSTGRES_USER>todolist</POSTGRES_USER>
                <POSTGRES_PASSWORD>todolist</POSTGRES_PASSWORD>
                <POSTGRES_DB>todo_list</POSTGRES_DB>
            </env>
            <ports>
                <port>postgres.port:5432</port>
            </ports>
            <wait>
                <log>database system is ready to accept connections</log>
                <time>20000</time>
            </wait>
        </run>
    </image>
</images>

Senza scendere nei dettagli, ci basta sapere che per creare un container abbiamo bisogno prima di una fase di “builda partire da un Dockerfile che genera un’immagine (cioè una sorta di container “archetipo” in sola lettura), e poi una di “run” che genera il container vero e proprio con la nostra configurazione.

Visto che l’immagine Docker di PostgreSQL 9.5 (definita dal nome:labelpostgres:9” su Docker Hub) è già pronta (cioè è già stata generata da un Dockerfile) e non abbiamo bisogno di generarne una nuova perché ci va bene così com’è, possiamo saltare direttamente alla fase di run, dove possiamo specificare una serie di parametri per il runtime:

Per verificare se la configurazione funziona, basta eseguire da riga di comando:

mvn docker:run

per avviare il database (l’unico configurato al momento). Per verificare se il container è veramente attivo (e a che porta ha effettuato il binding sull’host), possiamo usare direttamente il normale comando docker:

docker ps

Per fermare e cancellare il container:

mvn docker:stop

Siamo quindi in grado di ottenere dei test di integrazione automatici e portabili (perché il database viene scaricato, configurato e avviato automaticamente) e capaci di girare anche in un contesto di continuous integration perché non dobbiamo preoccuparci di conflitto di porte.

Questo tipo di configurazione è quindi ideale per la logica di business che si appoggia ad una base dati.
Un progetto di esempio si trova sul GitHub di CodingJam, dove il modulo Maven “todo-list-jaxrs-spring-services” avvia un container PostgreSQL per testare le query.

Ma che succede se vogliamo allargare il tiro, arrivando a fare dei veri e propri test end-to-end che includono anche il livello di API?

Tomcat, PostgreSQL e Docker: test end-to-end

Per fare un test di integrazione completo di tipo end-to-end è necessario avviare tutto lo stack necessario al funzionamento dell’applicazione: vedremo quindi come eseguire i test a partire dalle API REST, effettuando chiamate HTTP da JUnit tramite JAX-RS 2 client su un Tomcat avviato in un container Docker (dove è deployata la nostra applicazione) che dialoga con un altro container (in modo isolato) dove si trova PostgreSQL (dove è presente il nostro schema).

Aggiungiamo quindi l’immagine Docker di Tomcat al pom.xml:

<images>
    <image>
        <alias>todolist-tomcat</alias>
        <name>%g/todolist-tomcat:%l</name>
        <build>
            <args>
                <POSTGRES_DRV_VER>${postgresql.version}</POSTGRES_DRV_VER>
                <ARTIFACT>${project.build.finalName}.${project.packaging}</ARTIFACT>
            </args>
            <dockerFileDir>
                ${project.basedir}/src/it/resources/docker/tomcat
            </dockerFileDir>
            <assembly>
                <descriptorRef>artifact</descriptorRef>
            </assembly>
        </build>
        <run>
            <ports>
                <port>tomcat.port:8080</port>
            </ports>
            <wait>
                <http>
                    <url>http://${docker.host.address}:${tomcat.port}</url>
                </http>
                <time>30000</time>
            </wait>
            <links>
                <link>postgres-integration-test:db</link>
            </links>
        </run>
    </image>
    <image>
        <alias>postgres-integration-test</alias>
        ...
    </image>
</images>

Dal momento che dobbiamo personalizzare l’immagine Tomcat di Docker con la nostra applicazione e le dipendenze lato server (come il driver PostgreSQL per esempio), in questo caso useremo anche la fase di build dell’immagine, a partire da un Dockerfile. Rispetto al caso del database, abbiamo una situazione più articolata:

Il descrittore della fase di run in questo caso ci riserva due tag nuovi rispetto a quanto visto per il database:

Un esempio pratico si trova sul GitHub di CodingJam, nel pom.xml del modulo Maven “todo-list-jaxrs-spring-web“.

Conclusioni

La coppia Maven + Docker permette quindi di spingere le potenzialità dei test di integrazione fino ad un livello di affidabilità dei test mai visto fino adesso. I container Docker gestiti da Maven infatti non sono dei mock o dei surrogati dei sistemi da integrare, ma sono le vere istanze isolate in sandbox. Con questa soluzione quindi abbiamo raggiunto i tre obiettivi che ci eravamo prefissati: test automatici, capaci di controllare e configurare i sistemi integranti, rieseguibili e portabili tra un ambiente e l’altro.