Tests de beans CDI avec WeldSE et Arquillian

Un article de JTips.
Tests de beans CDI avec WeldSE et Arquillian
Auteur : Alexis Hassler
Formation(s) sur le sujet :
* Formation CDI
Sewatech.png


La question des tests unitaires se pose de façon répétitive, dès lors qu'on développe avec un modèle de composants basé sur un conteneur. Si CDI est prévu pour fonctionner dans JavaEE, le projet Weld fournit l'utilitaire Weld SE pour utiliser CDI en environnement JavaSE.

Les projets Weld et Seam 3 ne fournissent pas beaucoup d'aide pour les tests unitaires de CDI parce que leur stratégie de test pour tous les modèles de composants de JavaEE passe par Arquillian.

Dans cet article, nous verrons quelques techniques pour tester des composants CDI avec jUnit, grâce à Weld SE.

Before / After

Pour tester un composant CDI, il faut que Weld SE soit démarré au préalable, puis il faut demander l'instance du composant au conteneur.

Cette portion de code peut donc être intégrée dans une méthode d'initialisation de la classe de test, sans oublier une méthode de fin de tests pour arrêter Weld.

 public class TotoServiceTest {
 
     TotoService service;
     Weld weld;
     
     @BeforeClass
     public void initializeWeld() {
         weld = new Weld();
         WeldContainer container = weld.initialize();
         service = container.instance().select(TotoService.class).get();
     }
     
     @AfterClass
     public void shutdownWeld() {
         weld.shutdown();
     }
     
     ...
 }


Rule

Ce code peut être factorisé dans une Rule pour être utilisé depuis n'importe quelle classe de test.

 public class WeldSeRule extends ExternalResource {
 
     private Weld weld;
     private WeldContainer container;
     
     @Override
     protected void before() throws Throwable {
         weld = new Weld();
         container = weld.initialize();
     }
     
     @Override
     protected void after() {
         weld.shutdown();
     }
     
     public <T> T getBean(Class<T> testedClass) {
         return container.instance().select(testedClass).get();
     }
 }

Une fois cette Rule créée, on doit la déclarer dans chaque classe de test et appeler la méthode getBean() dans une méthode @Before.

public class TotoServiceTest {

 @Rule
 public WeldSeRule weld = new WeldSeRule();
     
     TotoService service;
     
     @Before
     public void init() {
         service = weld.getBean(TotoService.class);
     }
     ...
 }

Tout ça reste encore assez fastidieux, et la valeur ajoutée par rapport aux simples @BeforeClass et @AfterClass sont faibles. Il faudrait, pour que ce soit avantageux, injecter automatiquement l'objet à tester.


Runner

Cette troisième technique est utilisée avec Spring qui fournit un runner permettant à notre classe de test de devenir elle-même un bean géré et donc de supporter l'injection et toutes les autres fonctionnalités du framework. Un tel runner a existé dans le trunk de Weld, mais a été supprimé. Je suppose que pour faire quelque chose d'exhaustif, l'effort était important et que l'équipe de développement a préféré porter son effort sur Arquillian. Mon ambition ici est beaucoup plus raisonnable : je veux juste démarrer Weld SE puis tester une instance de ma classe de test gérée par Weld.

Mon runner est donc une classe qui hérite du runner par défaut, puis je redéfinis la méthode run pour ajouter le démarrage et l'arrêt de Weld SE. Enfin, je redéfinis la méthode createTest() pour lui faire retourner une instance créée dans Weld.

 public class WeldRunner extends BlockJUnit4ClassRunner {
     
     private Weld weld;
     private WeldContainer container;
     
     public WeldRunner(Class<?> klass) throws InitializationError {
         super(klass);
     }
     
     @Override
     public void run(RunNotifier notifier) {
         initializeWeld();
         super.run(notifier);
         shutdownWeld();
     }
     
     @Override
     protected Object createTest() throws Exception {
         return container.instance().select(getTestClass().getJavaClass()).get();
     }
     
     private void initializeWeld() {
         weld = new Weld();
         container = weld.initialize();
     }
     
     private void shutdownWeld() {
         weld.shutdown();
     }
 }

Il me suffit ensuite de lancer mes tests avec ce runner et de demander l'injection des objets à tester.

 @RunWith(WeldRunner.class)
 public class TotoServiceTest {
   
     @Inject
     TotoService service;
     
     @Test
     public void trouveParCleShouldWorkWithValidId() {
         Long id = 1L;
         Toto toto = service.trouveParCle(id);
         assertNotNull("Le toto n'a pas été trouvé.", toto);
         assertEquals("Le toto n'a pas la bonne clé.", id, toto.id);
     }
     ...
 }

Ce runner est certainement à améliorer, mais il fournit déjà le service que je lui demandais : initialiser mon environnement CDI sans polluer ma classe de test.


Arquillian

Arquillian est un outil développé par Red Hat / JBoss pour tester des composants gérés (CDI, EJB, JPA, JSF,...) dans leurs conteneurs. L'outil se compose des éléments suivants :

  • un Runner jUnit et des annotations associées,
  • des librairies pour embarquer les conteneurs,
  • Shrinkwrap, un outil pour empaqueter les classes à tester.

Au moment (janvier 2011) où j'écris ces lignes, Arquillian est en version alpha. Ceci se ressent avec des problèmes de compatibilité. Par exemple, la version alpha4 du conteneur Weld est compatible avec Weld 1.1.Beta1, mais pas avec la version finale. La situation devrait être rétablie avec les versions beta. En attendant, il faut utiliser une variante du conteneur fournie dans le projet Weld.

Par ailleurs, les versions alpha ne sont livrée que via Maven, et uniquement dans le repository public de JBoss.

Pour le développement, j'ai besoin des librairies suivantes :

  • org.jboss.weld:weld-core:1.1.0.Final, org.jboss.weld:weld-api:1.1.0.Final
  • org.jboss.spec:jboss-javaee-6.0:1.0.0.Final
  • org.slf4j:slf4j-log4j12:1.5.10, log4j:log4j:1.2.16

Pour la phase de test :

  • junit:junit:4.8.2
  • org.jboss.arquillian:arquillian-junit:1.0.0.Alpha4
  • org.jboss.weld.arquillian.container:arquillian-weld-ee-embedded-1.1:1.1.0.Final

Cette dernière librairie devra être remplacée par org.jboss.arquillian.container:arquillian-weld-ee-embedded-1.1, lorsque le projet aura atteint un meilleur niveau de stabilité. Au final, j'ai utilisé ce fichier pom.xml

Dans cet environnement, je peux développer mon test. Je dois tout d'abord utiliser le Runner Arquillian. Ensuite, je dois préparer l'archive qui sera déployée dans le conteneur. Enfin, j'injecte le bean à tester et je développe les cas de test.

 @RunWith(Arquillian.class)
 public class TotoServiceTest {
 
     @Deployment
     public static Archive<?> createDeployment() {
         return ShrinkWrap
                    .create(JavaArchive.class, "toto.jar")
                    .addClasses(TotoService.class)
                    .addManifestResource(EmptyAsset.INSTANCE, "beans.xml");
     }
 
     @Inject
     TotoService service;
 
     ...
 }

Le reste de la classe de test est tout à fait similaire à un test unitaire traditionnel.


Mock

Enfin, pour pouvoir tester correctement et de façon unitaire, il faut pouvoir introduire des objets de mock.

La meilleure façon de produire un objet de mock avec CDI est de développer une méthode annotée en @Produces directement dans notre classe de test. Le problème, c'est que ce bean va entrer en conflit avec le vrai bean et rompt la règle de Higlander : "There can be only one". La solution passe donc par la principe des alternatives.

Notre producteur de mock devient une alternative. Attention, c'est la classe qui contient la méthode de production qui doit être une alternative car la méthode ne peut pas être activée. Problème de granularité qui pourrait être corrigé dans un avenir proche.

 @Alternative
 public class TotoDaoMockProducer {
     @Produces TotoDao mockDao() {
         return mock(TotoDao.class);
     }
 }

Il ne reste plus qu'à activer cette alternative, en déclarant le stéréotype alternatif dans le beans.xml de test.

 <alternatives>
     <class>org.sewatech.cdi.test.TotoDaoMockProducer</class>
 </alternatives>

Le principal problème est d'activer cette alternative dynamiquement, sans avoir à modifier manuellement le fichier beans.xml. Une des sources de ce problème vient aussi du fonctionnement un peu trop automatique de CDI : tous les beans trouvés sont référencés et même, tous les fichiers beans.xml sont chargés.

La spécification CDI 1.0 ne prévoit malheureusement pas de technique de configuration dynamique, par programmation. Cette fonctionnalité a été recensée dans les évolutions envisageables pour CDI 1.1. Il est peut-être possible d'arriver au même résultat en attaquant le moteur Weld, mais je n'ai pas réussi à le faire.

L'autre solution passe par Arquillian. Le fichier beans.xml peut être généré via ShrinkWrap, ce qui apporte la souplesse qu'on attendait.

 @Deployment
 public static Archive<?> createDeployment() {
     String testBeansXml = "<beans xmlns=\"http://java.sun.com/xml/ns/javaee\""
                                + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                + "xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd\">"
                             + "<alternatives><class>org.sewatech.cdi.service.TotoDaoMockProducer</class></alternatives>"
                         + "</beans>";
         return ShrinkWrap
                    .create(JavaArchive.class, "toto.jar")
                    .addPackage(TotoService.class.getPackage())
                    .addPackage(TotoDao.class.getPackage())
                    .addPackage(ConnectionManager.class.getPackage())
                    .addManifestResource(new ByteArrayAsset(testBeansXml.getBytes()), "test/beans.xml")
                    .addManifestResource("META-INF/beans.xml", "beans.xml");
     }
 }

Dans cet exemple, nous utilisons le fichier beans.xml classique et un fichier test/beans.xml construit dynamiquement.

La solution est cocasse : on utilise un outil dont le but est de faciliter les tests d'intégration pour exécuter des tests unitaires.


Quelques autres articles sur CDI
Quelques autres articles sur JUnit