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:
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:
org.jboss.arquillian.junit arquillian-junit-container org.jboss.arquillian.protocol arquillian-protocol-servlet org.jboss.shrinkwrap.resolver shrinkwrap-resolver-impl-maven
(che sono in scope test per via del bom jboss-javaee-7.0-with-tools), ma soprattutto i due profili che fanno la magia:
arq-wildfly-managed org.wildfly wildfly-arquillian-container-managed test arq-wildfly-remote org.wildfly wildfly-arquillian-container-remote test
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:
org.apache.maven.plugins maven-failsafe-plugin 2.18.1 integration-test verify
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:
org.jboss.shrinkwrap.resolver shrinkwrap-resolver-impl-maven test
(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() { Optionaluser = 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
che IntelliJ
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("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
192.168.100.123 9990
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:
arq-wildfly-quality org.wildfly wildfly-arquillian-container-remote test org.apache.maven.plugins maven-failsafe-plugin 2.18.1 integration-test verify qa_jboss
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:
org.apache.maven.plugins maven-dependency-plugin 2.8 unpack process-test-classes unpack org.wildfly wildfly-dist ${version.wildfly} zip false target
che viene decompresso nella cartella target prima di eseguire i test. Qualche ritocco al failsafe-plugin:
org.apache.maven.plugins maven-failsafe-plugin 2.18.1 integration-test verify qa_jboss org.jboss.logmanager.LogManager ${project.basedir}/target/wildfly-10.0.0.Final false
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.