Java EE integration test con Arquillian

Java Enterprise e test di integrazione con Arquillian

Scrivere i test durante lo sviluppo (o prima, se fate TDD) è molto importante. L’uso quotidiano di framework di mocking come Mockito o EasyMock (magari aiutati dal mitico PowerMock) hanno notevolmente semplificato la scrittura di test di unità permettendo di svincolarsi dai framework e dal resto del mondo che circonda la funzionalità che stiamo sviluppando.
Ad un certo punto però bisogna comunque fare i conti con il mondo reale ed integrare le parti: ed è qua che, almeno nel contesto Java Enterprise, le cose si complicano. Avviare un contesto Spring, soprattutto in fase di test, è molto semplice. Purtroppo in Java EE puro non è mai stato così, almeno fino a prima di vedere Arquillian all’opera. Arquillian non è certo uscito ieri, ma vista la velocità con cui si avviano alcuni application server di oggi e lo sviluppo molto orientato alle API, adesso è molto più facile scrivere test di integrazione, direttamente sui server veri.

Fino a ieri

Fino a qualche anno fa infatti molti application server erano dei carrozzoni che impiegavano minuti ad avviarsi, per cui era lento e noioso avere un feedback da un test di integrazione: ammesso che si riuscisse a fare un deploy automatico, si riusciva solo a testare tutto lo stack (dal web al db per intendersi), per non parlare dei problemi del tipo: chi avvia il server in automatico? E quando?

La scorciatoia quindi è sempre stata quella di aggirare il problema cercando di avviare dal test dei “contesti” che simulassero al meglio parte dell’ambiente messo a disposizione dall’application server: mentre è relativamente facile avviare un vero contesto JPA o CDI, un po’ meno lo è per EJB (a meno di non mettere in mezzo OpenEJB o gli Embedded Container, disponibili da EJB 3.1): di fatto però si tratta di contesti esterni che assomigliano molto a quelli forniti dal server in cui girerà l’applicazione, ma non uguali, quindi la possibilità di avere dei test con falsi positivi è molto alta.

Arquillian invece adotta il principio opposto, ovvero “what you test is what you run“: permette infatti di fare il deploy sul server reale delle sole funzionalità inerenti al test, permettendo anche di controllare l’avvio e lo stop del server. Quello che testi quindi viene eseguito nell’ambiente reale in cui girerà a regime.
Con l’aiuto poi di ShrinkWrap, Arquillian crea dei piccoli jar, war o ear (chiamati “micro deployments“) contenenti solo quello che vogliamo testare (compresa la classe di test stessa!), rendendo più veloce la fase di deploy.

Ma di che server target stiamo parlando? Arquillian è capace di dialogare con numerosi server, da JBoss a GlassFish, da Tomcat o Jetty al LibertyProfile di IBM tramite opportuni moduli. Non solo, può anche avviare semplici container come CDI (Weld) o EJB (OpenEJB). Tutti i dettagli sulle configurazioni possono essere trovati sulla documentazione ufficiale.

Questo post è basato su test JUnit eseguiti su WildFly 10. Cerchiamo di capire quindi come si fa a mettere in piedi un test di integrazione.

Prerequisiti

Visto che gli archetipi Maven dei progetti per JBoss prevedono già Arquillian pronto all’uso, è più conveniente partire da una situazione funzionante invece che perdersi in eventuali configurazioni. Il progetto di esempio che sta alla base di questo post, ovvero la classica API per creare e consultare “Todo List”, si trova su GitHub, e risponde ai seguenti prerequisiti:

  • E’ necessario aver scaricato un application server JBoss: in questo caso WildFly 10 (10.0.0.Final).
  • La variabile d’ambiente JBOSS_HOME deve puntare alla cartella root di WildFly (dopo vedremo perché)

Partendo quindi dall’archetipo Maven per WildFly (per esempio da riga di comando):

mvn archetype:generate -DarchetypeGroupId=org.wildfly.archetype -DarchetypeArtifactId=wildfly-javaee7-webapp-ear-blank-archetype

verrà generato un progetto con la classica struttura a tre moduli:

my-project
 - my-project-ear
 - my-project-ejb
 - my-project-web

Solo il progetto my-project-ejb è pronto per Arquillian: tra le risorse, nella cartella my-project-ejb/src/main/resources, troviamo infatti il file arquillian.xml. E’ il file di configurazione principale, ricercato automaticamente nella root del classpath al momento del lancio di un test per determinare dove e su che tipo di server fare il deploy:

<arquillian xmlns="http://jboss.org/schema/arquillian"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://jboss.org/schema/arquillian
        http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

   <!-- Force the use of the Servlet 3.0 protocol with all containers, as it is the most mature -->
   <defaultProtocol type="Servlet 3.0" />

   <!-- Example configuration for a remote WildFly instance -->
   <container qualifier="jboss" default="true">
        <!-- By default, arquillian will use the JBOSS_HOME environment variable. -->
   </container>

</arquillian>

Senza modificare nessuna configurazione, Arquillian farà il deploy su qualsiasi JBoss server che risponde alla variable d’ambiente JBOSS_HOME, ovviamente su localhost. Per il momento ci va più che bene, più avanti vedremo come fare un deploy su un server remoto. In questo modo è possibile usare per i test il server vero e proprio, compreso il datasource JTA o i profili di sicurezza reali, piuttosto che le code JMS o qualsiasi altro servizio dell’application server solitamente disponibile.

Guardando invece il pom.xml sempre del progetto my-project-ejb, notiamo le dipendenze di Arquillian:

<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
</dependency>
<dependency>
    <groupId>org.jboss.arquillian.protocol</groupId>
    <artifactId>arquillian-protocol-servlet</artifactId>
</dependency>
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
</dependency>

(che sono in scope test per via del bom jboss-javaee-7.0-with-tools), ma soprattutto i due profili che fanno la magia:

<profile>
    <id>arq-wildfly-managed</id>
    <dependencies>
        <dependency>
            <groupId>org.wildfly</groupId>
            <artifactId>wildfly-arquillian-container-managed</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</profile>
<profile>
    <id>arq-wildfly-remote</id>
    <dependencies>
        <dependency>
            <groupId>org.wildfly</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</profile>

I nomi sono molto parlanti: arq-wildfly-managed esegue i test di integrazione in un server gestito da Arquillian stesso, avviato quindi nella stessa JVM del test. Il framework si preoccupa quindi di avviare il server, deployare l’artefatto, eseguire i test, eseguire l’undeploy e fermare il server. Anche se WildFly è estremamente veloce ad avviarsi, i tempi di risposta dell’intero processo possono essere considerati non accettabili durante lo sviluppo. In fase di sviluppo quindi è più conveniente usare il profilo arq-wildfly-remote: Arquillian deployerà l’artefatto su un server già avviato su localhost (nella configurazione attuale), eseguirà i test ed effettuerà l’undeploy. Per Arquillian “remote” significa differente JVM rispetto a quella che esegue il test, ma può voler dire anche macchina differente, come vedremo più avanti.

Prima di cominciare

A mio avviso la configurazione delle dipendenze Arquillian e i profili mutuati dall’archetipo non sono proprio adatti ad un progetto “reale” perché i test vengono eseguiti da surefire (nella fase di test di Maven, come se fossero considerati di unità) invece che da failsafe (in fase di integration-test), come abbiamo già avuto modo di scoprire tempo fa.

Nel progetto di esempio di GitHub sono presenti quindi le seguenti modifiche:

  • aggiunto il modulo Maven “test-utils che contiene tutte le dipendenze di Arquillian (spostate dal progetto ejb) e le classi di utilità di test (che vedremo tra poco)
  • i profili arq-wildfly-managed e arq-wildfly-remote sono stati invece spostati nel parent pom e arricchiti con il plugin failsafe, in modo da eseguire i test nella fase di integration-test:
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.18.1</version>
        <executions>
            <execution>
                <goals>
                    <goal>integration-test</goal>
                    <goal>verify</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    

Arquillian all’opera: setup di un test

Cominciamo subito testando il modulo ejb che definisce un Session Bean Stateless per il CRUD dei task (TaskService).

Per prima cosa è necessario preparare il test e il deployment:

@RunWith(Arquillian.class)
public class TaskServiceIT {

    @Deployment
    public static Archive<?> createDeployment() {
        ...
    }
...
}

L’annotazione @RunWith(Arquillian.class) sulla classe di test di integrazione (TaskServiceIT.java, da notare il suffisso IT, come vuole failsafe) richiede a sua volta un metodo statico e pubblico, annotato con @Deployment, che restituisce un Archive, ovvero il nostro “micro deployment”.

Micro deployment

Come abbiamo già detto, Arquillian permette di eseguire i test su una versione ridotta della nostra applicazione, deployando solo le classi necessarie. Come si crea questo artefatto? Grazie a ShrinkWrap possiamo creare un oggetto Java che rappresenta un archivio deployabile, per esempio:

@Deployment
public static Archive<?> createDeployment() {
    return ShrinkWrap.create(JavaArchive.class, "arquillian-tests-ejb.jar")
        .addPackages(true, "it.codingjam.arquilliantests.logic", "it.codingjam.arquilliantests.utils")
        .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
        .addAsManifestResource("META-INF/persistence.xml", "persistence.xml")
        .addAsResource("META-INF/orm.xml")
        .merge(getMavenDependencies());
}

Vediamo nel dettaglio:

  • create: crea un oggetto JavaArchive che rappresenta un jar di nome arquillian-tests-ejb.jar.
  • addPackages: aggiunge al jar ricorsivamente (tramite il primo parametro booleano) i package specificati
  • addAsManifestResource: aggiunge risorse alla cartella META-INF del jar. Nel primo caso viene creato al volo un bean.xml vuoto per attivare CDI; nel secondo invece, si dice di usare il file META-INF/persistence.xml del nostro progetto come persistence.xml del jar che si sta “impacchettando”.
  • addAsResource: versione generica del metodo precedente, crea una risorsa nel jar. Questo metodo, come il precedente, ha diversi overload: quando è usato con un singolo parametro, significa che sorgente e destinazione coincidono.
  • merge: visto che si tratta di un jar, non è possibile aggiungere le dipendenze Maven necessarie come si farebbe in un war o in un ear. E’ necessari creare quindi uno uber-jar, fondendo (merge appunto) le dipendenze con le nostre classi. getMavenDependencies infatti ritorna un jar contenente tutte le dipendenze necessarie e verrà unito al micro deployment.
  • private static JavaArchive getMavenDependencies() {
       return ShrinkWrap.createFromZipFile(JavaArchive.class,
                                     resolveMavenDependencies().asSingleFile());
    }
    

    Cosa fa quindi resolveMavenDependencies?

Gestione delle dipendenze

E’ difficile pensare ad un progetto senza dipendenze di terze parti, di conseguenza ci deve essere un modo per includere anche nel micro deployment che stiamo creando. Per mettere d’accordo ShringWrap con Maven è necessario aggiungere una dipendenza:

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <scope>test</scope>
</dependency>

(in questo progetto la versione viene ereditata dal BOM di WildFly). A questo punto possiamo implementare resolveMavenDependencies

private static MavenFormatStage resolveMavenDependencies() {
   return Maven.resolver()
            .loadPomFromFile("pom.xml")
            .importRuntimeDependencies()
            .resolve("commons-codec:commons-codec")
            .withTransitivity();
}

  • resolver: crea un’istanza del risolutore delle dipendenze Maven.
  • loadPomFromFile: configura il resolver con il pom.xml specificato.
  • importRuntimeDependencies: a discapito del nome, aggiunge al resolver le dipendenze in scope compile, system, import e runtime.
  • resolve: avvia il vero e proprio meccanismo di risoluzione delle dipendenze, specificate nella forma canonica “groupId:artifactId“.
  • withTransitivity: risolve ricorsivamente le dipendenze specificate dal metodo precedente.

Il mio primo test di integrazione

Dal momento che anche la classe di test farà parte del micro deployment, è possibile usare @Inject per iniettare nel test la classe da testare (TaskService):

@Inject
private TaskService taskService;

@Test
public void shouldNotFindTask() {
   Optional<Task> user = taskService.getBy(-1, "testUser");
   assertFalse(user.isPresent());
}

Per il resto, niente di nuovo! Dal momento che la classe di test risulta un bean deployato a tutti gli effetti, possiamo anche usare l’EntityManager o la UserTransaction per preparare il database o svuotarlo (come per esempio la classe TestDbUtils).

A questo punto siamo pronto a lanciare il test di integrazione (con Maven per esempio), ricordandoci di attivare il profilo desiderato (arq-wildfly-managed o arq-wildfly-remote). Nel caso in cui si voglia far gestire il server ad Arquillian:

mvn verify -Parq-wildfly-managed

Se lanciato da una IDE, è necessario attivare il profilo prima di lanciare il test, sia in Eclipse

Java EE integration test con Arquillian: Eclipse Select Maven profile

che IntelliJ

Java EE integration test con Arquillian: Intellij Select Maven Profile

Una volta lanciato, guardando la console, si noterà ad un certo punto che verrà avviato il server, e verrà deployato un war!!

WFLYUT0021: Registered web context: /test
WFLYSRV0010: Deployed "test.war" (runtime-name : "test.war")

Come mai? Non in tutti gli application server è possibile deployare un jar, quindi Arquillian genera un archivio war wrapper per poter effettuare il deploy ed eseguire i test.

Ovviamente ShrinkWrap permette di creare anche un war per testare il modulo web, in modo molto simile a quanto visto per il jar:

@Deployment
public static Archive<?> createDeployment() {
    return ShrinkWrap.create(WebArchive.class, "arquillian-tests-web.war")
         .addPackages(true, "it.codingjam.arquilliantests", "it.codingjam.arquilliantests.utils")
         .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
         .addAsResource("META-INF/persistence.xml")
         .addAsResource("META-INF/orm.xml")
         .addAsLibraries(resolveMavenDependencies().asFile());
}

WebArchive offre qualche metodo in più rispetto a JavaArchive:

  • addAsWebInfResource: aggiunge risorse alla cartella WEB-INF. Come nel caso del jar, è necessario avere un bean.xml per attivare CDI.
  • addAsLibraries: si aggiungono le dipendenze (Maven in questo caso) all’archivio come si farebbe normalmente.

Deployando questo archivio web, si scopre che viene associato un context path con lo stesso nome del war:

WFLYUT0021: Registered web context: /arquillian-tests-web
WFLYSRV0010: Deployed "arquillian-tests-web.war" (runtime-name : "arquillian-tests-web.war")

è possibile quindi testare le nostre API REST con una vera e propria chiamata HTTP, magari con l’API client di JAX-RS 2:

@Test
public void shouldGetTaskForUser1() {
   WebTarget target = ClientBuilder.newClient().target("http://localhost:8080/arquillian-tests-web/api/v1");
   Response response = target.path("todos/1").queryParam("user", "user1")
            .request()
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .get();

   assertNotNull(response);
   assertEquals(200, response.getStatus());
}

Debug di un test

Sembrerebbe un argomento scontato ma merita un cenno. La modalità “managed” (ovvero il profilo arq-wildfly-managed) non è compatibile con il debug: avviando infatti il test in debug e mettendo un breakpoint, si ha la spiacevole sorpresa che viene ignorato! Come fare quindi? La modalità “remote” (arq-wildfly-remote) ci viene in aiuto! L’importante è avviare il server in modalità debug dalla nostra IDE preferita: dal momento che sappiamo che la classe di test viene deployata, è facile aspettarsi di debuggare il test come se fosse una classe della nostra applicazione.

Ulteriori configurazioni

Server remoto

Fin qua abbiamo usato Arquillian senza toccare la configurazione base. Nel caso di debug, abbiamo parlato di “server remoto”, ma di fatto siamo sempre su localhost. In generale può fare comodo eseguire il deploy remoto su un server che risiede in un’altra macchina: come fare quindi? Nel caso di WildFly è molto semplice: aggiungiamo un nuovo blocco container al file arquillian.xml

<container qualifier="qa_jboss">
    <configuration>
        <property name="managementAddress">192.168.100.123</property>
        <property name="managementPort">9990</property>
    </configuration>
</container>

Questo nuovo container ha un nome identificativo (qa_jboss) che useremo per attivarelo. Per questo nuovo container è possibile configurare: managementAddress, managementPort, username e password. In questo caso quindi abbiamo ipotizzato di avere il server già avviato all’indirizzo 192.168.100.123 con management console alla porta 9990, senza autenticazione.

Per usare questo nuovo container basta creare un nuovo profilo Maven, chiamato per esempio arq-wildfly-quality, identico alla versione remote, ma dove si specifica il container da usare:

<profile>
    <id>arq-wildfly-quality</id>
    <dependencies>
        <dependency>
            <groupId>org.wildfly</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.18.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                        <configuration>
                            <systemPropertyVariables>
                                <arquillian.launch>qa_jboss</arquillian.launch>
                            </systemPropertyVariables>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

che è equivalente a passare il parametro -Darquillian.launch=qa_jboss se non si volesse creare un nuovo profilo.

Autodownload del server

Una strada totalmente inversa a quella vista adesso è quella non avere nessun server già preparato o configurato, ma permettere a Maven di scaricarlo al volo prima di lanciare i test. Personalmente ritengo che in contesto lavorativo non è un caso d’uso frequente perché molto spesso l’application server ha sempre qualche configurazione personalizzata, come il datasource piuttosto che i driver JDBC, la security o le code JMS. A scopo didattico però è bene sapere che si può fare, basta aggiungere il plugin Maven per scaricare WildFly come dipendenza:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>2.8</version>
    <executions>
        <execution>
            <id>unpack</id>
            <phase>process-test-classes</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.wildfly</groupId>
                        <artifactId>wildfly-dist</artifactId>
                        <version>${version.wildfly}</version>
                        <type>zip</type>
                        <overWrite>false</overWrite>
                        <outputDirectory>target</outputDirectory>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

che viene decompresso nella cartella target prima di eseguire i test. Qualche ritocco al failsafe-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.18.1</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <systemPropertyVariables>
                    <arquillian.launch>
                        qa_jboss
                    </arquillian.launch>
                    <java.util.logging.manager>
                        org.jboss.logmanager.LogManager
                    </java.util.logging.manager>
                    <jboss.home>
                        ${project.basedir}/target/wildfly-10.0.0.Final
                    </jboss.home>
                </systemPropertyVariables>
                <redirectTestOutputToFile>false</redirectTestOutputToFile>
            </configuration>
        </execution>
    </executions>
</plugin>

ed il gioco è fatto.

Conclusioni

I test di integrazione nel mondo Java EE sono sempre stati un tallone d’Achille: adesso abbiamo uno strumento dalla nostra parte che ci permette di eseguire i test su un’istanza reale del server target, senza creare contesti “fasulli” per ovviare alla mancanza di uno standard sui test di integrazione nella specifica Java Enterprise.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+