vendredi 29 novembre 2013

Tester son application JavaEE avec Arquillian

La bonne pratique de faire des tests unitaires dans son application est rentrée dans les moeurs du développeurs Java, JUnit étant le plus connu et utilisé. Il permet de répondre facilement aux cas de tests standards. Mais les problématiques arrivent très vite dès lors que l’on souhaite tester en environnement EE : comment effectuer des tests unitaires sur les EJB, sur les services d’accès aux données, comment prendre en compte l’aspect transactionnel et les données sans polluer une base existante ?

Autant de questions qui peuvent trouver une réponse grâce à Arquillian, un framework de test pour JavaEE.


Arquillian permet de tester son code dans un environnement proche des conditions réelles de l’application, c’est à dire au sein d’un conteneur JavaEE. A ce titre, Arquillian propose différents type de conteneurs pour ces tests.

Quelle différence entre le mode de conteneur “Embedded”, “Managed” et “Remote” ?
On peut dissocier le mode embarqué (Embedded) du mode “In-container” (Managed ou Remote).
Le mode embarqué (Embedded) est une “émulation de conteneur” : le process du mode embarqué est exécuté dans le même process que le test et agit comme un conteneur mais n’en est pas un. Il y’a donc des risques de différences de comportement entre ce mode et le conteneur réel. De plus, le mode embarqué se réduit à l’injection CDI : certaines fonctionnalité ne sont pas prises en compte (JPA) réduisant son intérêt dans certains cas.

Le mode in-container (Managed ou Remote) tire parti d’un conteneur réel qui peut être distant ou managé.
Le mode distant (Remote) utilise un conteneur hors de l’environnement de test. Ce mode présume que le conteneur soit déjà démarré : lors de l’exécution du test unitaire, Arquillian n’a pas la main sur le démarrage/arrêt du conteneur. C’est donc au développeur de s’assurer que le conteneur soit démarré avant de lancer ses tests. Le mode embarqué est conseillé en phase de développement pendant lequel le développeur peut placer le conteneur en Debug et ainsi faciliter l’analyse et la correction de bugs.

Le mode managé (managed) est simplement un conteneur distant qui prend également en compte son cycle de démarrage et d’arrêt. Contrairement au conteneur embarqué, le processus du conteneur managé est exécuté dans un process JVM séparé. Une fois que le serveur est démarré, il se comporte comme le conteneur distant, c’est à dire qu’Arquillian intéragit avec lui à travers d’un protocole distant (comme HTTP).
Le conteneur managé est plus utile dans le cadre d’un environnement d’intégration continue dans lequel l’on souhaite tester sur un conteneur complet, mais sans savoir s’il est déjà démarré. Arquillian s’occupe donc de démarrer le conteneur avant d’effectuer les tests, puis de l’éteindre une fois qu’ils sont terminés.

micro-déploiement par classe de tests

L’utilisation d’Arquillian sur les cas classiques est très bien expliqué sur le tutoriel du site, je ne vais donc pas m’attarder sur le sujet et redirige donc vers le site officiel : http://arquillian.org/guides/getting_started/ beaucoup mieux expliqué que je ne le ferais, je vous conseille donc d'y jeter un oeil.

En résumé, pour réaliser un test Arquillian, il faut créer une classe de tests annotée @RunWith(Arquillian.class). Cette annotation précise à JUnit d’utiliser Arquillian comme controleur de test. Arquillian recherche ensuite toutes les méthodes annotées avec @Deployment pour récupérer l’archive de test que l’on appelle le micro-deploiement. Le but de ce micro-déploiement est d’isoler les classes et les ressources qui sont utilisées pour le tests.

Contrairement à un test normal JUnit, Arquillian ne s’appuie pas sur l’intégralité du Classpath. Il faut indiquer seulement les ressources dont nous avons besoin pour les tests. L’archive est crée avec Srinkwrap, l’API de création d’archive (JAR, EAR, WAR). Cette approche permet de concentrer les tests exclusivement sur les classes utiles et permet de garder des tests propres et maintenables.
@Deployment
public static WebArchive createDeployment() {
  
    WebArchive webArchive = ShrinkWrap.create(WebArchive.class)
                .addAsWebInfResource("META-INF/ejb-jar.xml")
                .addAsWebInfResource("META-INF/beans.xml")
                .addPackages(true, "fr.lynchmaniac.arquilliantest");
    return webArchive;
}
Le composant à tester est injecté en annotant l’attribut @EJB ou @Inject.
@EJB
WeatherWs ws;
Le positionnement des annotations @Test définir les méthodes de tests constitue les méthodes de tests unitaires. Sur ce dernier point, les tests Arquillian ressemblent comme deux gouttes d'eau à un test JUnit.
@Test
public void rainingTest() {
    Assert.assertFalse(ws.isRainingInAnHour("Nantes"));
}
Comme expliqué précédemment, il est préférable d’utiliser le mode Remote ou Managed. Dans notre cas, nous utilisons JBoss 7.1.1, les dépendances tirées sont donc, pour le mode Managed :
<dependency>
    <groupid>org.jboss.as</groupid>
    <artifactid>jboss-as-arquillian-container-managed</artifactid>
    <version>7.1.1.Final</version>
    <scope>test</scope>
</dependency>
Et pour le mode remote, nous utilisons :
<dependency>
    <groupid>org.jboss.as</groupid>
    <artifactid>jboss-as-arquillian-container-remote</artifactid>
    <version>7.1.1.Final</version>
    <scope>test</scope>
</dependency>
A noter que les artefacts suivants ne sont pas disponibles par défaut sur le dépôt Central Maven. Il faudra ajouter les dépôts suivants :
<repository>
    <id>jboss.org</id>
    <url>https://repository.jboss.org/nexus/content/repositories</url>
</repository>
    
<repository>
     <id>JBoss 3rd Party Releases</id>
  <url>https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/</url>
</repository>

Quelques conseils : 
  • Attention à n’ajouter que ce dont vous avez besoin au micro-déploiement. Si vous avez besoin d'une classe, n'ajoutez pas le package complet. En effet, ajouter trop de classe dans une archive augmente de temps de création et de déploiement. 
  • Les classes de tests (i.e. celles qui lancent les tests), n’ont pas besoin d’être présente dans l’archive, veillez à les exclure du micro-déploiement. 
  • Attention à bien définir la variable d’environnement JBOSS_HOME qui est nécessaire, en mode Managed pour lancer le serveur. 
  • Réduire au maximum le chargement des modules JBoss afin d’en réduire autant que possible le temps de chargement. Il peut être utile de créer son propre standalone.xml utilisé exclusivement pour le passage des TU. Dans notre cas de tests, nous avons réduit au chargement des modules suivants dans JBoss: {org.jboss.as.} configadmin,connector, ee, ejb3, jmx, jpa, logging, naming, osgi, remoting, security, threads, transactions, web, webservices,weld 
  • Définir un profil Maven de développement, actif par défaut, qui inclus l’artefact pour le déploiement Arquillian en mode Remote. Un profil "ci" peut être créé pour inclure l’artefact en mode "managed".
  • Dans le cas pour lequel vous souhaiter intégrer des artefacts projet à votre micro-déploiement via le Dependency Resolver, veillez à garder en tête les phases du cycle de vie Maven : test -> package -> install. Si vous souhaitez tirer parti d’un artefact pendant les tests, il faudra prévoir un passage de tests en 2 étapes : installer d’abord l’artefact en skippant les tests (mvn install -DskipTests) pour qu’il soit présent dans le dépôt, puis ensuite effectuer le test (mvn test).

Micro-déploiement par ensemble de classe de tests

Cette approche assure un premier niveau de test basique et fonctionnel. Il s’avère que sur un projet plus conséquents, comportant un grand nombre de classes de tests, cette approche s’avère très longue lors du passage des TU. En effet, chaque classe de test définit son propre micro-déploiement, mais nécessite également des phases de déploiement et de retrait des archives (en l'occurrence très souvent des WAR). Il s’agit clairement d’une limitation dans notre cas, car la création et le déploiement d’une archive est très coûteuse en temps. De ce fait, un grosse partie du temps de passage des TU est passé dans les tâches qui pourraient être mutualisées : création, déploiement et retrait de l’archive.


 En effet, pour des raisons d’optimisation, il est plus judicieux, d’augmenter le nombre d'éléments dans le micro-déploiement (qui sera un peu plus gros), mais d’assurer cette création et ce déploiement une seule fois pour tous les tests. Nous laissons tomber le déploiement par classe de test mais assurons un déploiement par unité " fonctionnelle ". L’objectif est donc de mutualiser la création et le déploiement afin d’éviter qu’il y ait un surcoût pour chaque nouveau test.

 Nous utilisons une extension d'Arquillian afin de détourner la cinématique de test à des fins d'amélioration de temps de passage des tests. La cinématique de test est donc la suivante :


 Arquillian ne propose pas cette fonctionnalité par défaut à l’heure actuelle (prévue dans la Roadmap de la version 2.0), mais nous allons voir comment tirer partie des extensions pour effectuer un micro-déploiement mutualisé pour une Suite de test.

Définition de la classe d’extension.
La classe d’extension ArquillianSuiteExtension est une classe technique qui implémente org.jboss.arquillian.core.spi.LoadableExtension. Je n’entre pas en détail dans l’implémentation de cette classe, les sources sont disponibles sur Github. Elle intercepte les évènements du cycle de vie d’Arquillian (démarrage, deploy, etc.). L’idée derrière cette classe est d’intercepter le démarrage d’un test Arquillian et de forcer à constituer le déploiement à partir d’une classe spécifique, au lieu de le lire dans chaque classe de test.
La classe contenant le déploiement est indiqué dans le fichier de configuration arquillian.xml :
<extension qualifier="suite">
    <property name="deploymentClass">fr.lynchmaniac.test.Deployments</property>
</extension>

Dans notre exemple, le nom qualifié ici fr.lynchmaniac.test.Deployments désigne la classe chargée de mutualiser la création de l’archive de test pour l’ensemble du projet.

Enregistrement de la classe d’extention
Pour être prise en compte, la classe d’extension doit enregistrée. C’est SPI qui est utilisé pour prendre en compte cette extension. le fichier META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension doit être présent dans le CLASSPATH et contenir le chemin vers la classe d’extension :
fr.lynchmaniac.test.ArquillianSuiteExtension

Classe de définition du micro-déploiement
La classe fr.lynchmaniac.test.Deployments doit contenir une méthode annoté @Deployment qui constituera le micro-déploiement complet pour un domaine. 

Attention : contrairement au premier chapitre, dans le cadre d’un déploiement mutualisé, les classes de tests doivent être présentes dans l’archive du micro-déploiement.

 @Deployment
    public static WebArchive deploy() {
    
     WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "ARQUILLIAN-TEST.war");
        
        webArchive.addPackages(true, "fr.lynchmaniac.arquilliantest")
                // dans ce cas, la classe de test doit être inclus dnas le WAR !
                .addClass(WeatherWsTest.class)
  .addClass(WeatherWs2Test.class)
                .addAsWebInfResource("META-INF/ejb-jar.xml")
                .addAsWebInfResource("META-INF/beans.xml");
 
     return webArchive;

    }

Les classes de tests
Cette classe ne doit surtout pas contenir d'annotation @Deployments, mais doit posséder au moins une annotation @Test.
@RunWith(Arquillian.class)
public class WeatherWsTest {

    @EJB 
    WeatherInfoService wis;
 
    @Test
    public void rainingTest() {
        Assert.assertFalse(wis.isRainingInAnHour("Nantes"));
    }
}

En conclusion

Ce premier article sur Arquillian, réalisé conjointement avec Vincent PIARD, a été rédigé dans l’objectif de donner un retour d’expérience sur les problématiques rencontrées pour les tests unitaires sur des architectures JavaEE. Nous avons utilisé Arquillian depuis plus d’un an sur un projet complexe et ce framework nous a aidé à les résoudre en partie.

En dernier conseil, il est important de garder à l’esprit plusieurs éléments essentiels pour faciliter les développements de tests unitaires dans ce cas :

  • Assurer un couplage lâche des composants est essentiels pour le passage des tests. Ceci permet de faciliter la création de mock et d’assurer une meilleure isolation des tests. 
  • Ne pas sur-découper les projets dans l’IDE et les artefacts (JAR en l’occurence). Un découpage fonctionnel ne veut pas obligatoirement dire un découpage technique" (i.e. un projet IDE/maven différent). Avoir trop de découpage en sous module technique complexifie les dépendances et le développement dans l'IDE. Cette complexité rend la constitution des micro-déploiements difficile à gérer.
  • Il est illusoire de penser que tous les TU doivent avoir une couverture de test de 100%. Vouloir atteindre 100% de couverture de test est illusoire et coûtera bien trop cher par rapport à la valeur ajoutée ! Préférez une approche réaliste : rester dans une fourchette de 2 à 5 tests. L’idéal est à minima de tester avec des entrées vides et au minimum et de tester avec des entrées complètes (cas à vide et cas optimal).
Dans le prochain article, nous verrons comment tirer parti d’Arquillian pour effectuer des tests de persistances.

Pour aller plus loin


Pour voir le détail de l'implémentation notamment toute la mécanique technique portée par ces tests, je vous encourage à jeter un oeil aux sources disponibles sur Github.

Autres sources intéressantes :