jeudi 11 novembre 2010

Utiliser Glassfish Embedded pour tester ses EJB 3.1

Depuis la sixième version de Java EE, il est possible de tester ses EJB avec l'API  "EJB Embedded Container". Glassfish propose son implémentation intitulée "glassfish-embedded".


Cet article a un double objectif :
  • Présenter "Glassfish Embedded" et en quoi son utilisation facilite grandement le test de ses EJB ;
  • Lever certaines interrogations et fausses rumeurs selon lesquelles il serait impossible de tester ses EJB avec une base de données autre que JavaDB. En effet, "Glassfish Embedded" est fourni avec une source de données JavaDB embarquée accessible via l'entrée JNDI suivante : jdbc/__default. Or, comment faire si je veux tester un EJB qui manipule des entités JPA devant être synchronisées avec un support de persistance tel que MySQL par exemple ?

1/ Configuration du projet

Dans cet article, j'ai utilisé un projet Maven 2 afin de ne pas être dépendant de quelqu'IDE que ce soit. Les dépendances utilisées sont l'API Java EE 6, JUnit et Glassfsih Embedded (cliquez sur l'image pour agrandir) :





Pour que tout fonctionne, il faut bien entendu configurer les dépôts et les plugins (cliquez sur l'image pour agrandir) :





















































L'API Java EE 6 propose également certaines mises à jour des API de base fournies avec Java. Aussi, si l'on souhaite disposer de ces mises à jour, il faut configurer un Profile (cliquez sur l'image pour agrandir) :




2/ Création d'un EJB Hello World

Maintenant que notre projet est configuré, passons à l'implémentation de nos EJB. Commençons par un EJB simplissime : un "Hello World !" :



Vous noterez que notre EJB est un simple POJO annoté. En effet, la version 3.1 a considérablement simplifié l'écriture de ces composants. L'annotation @Stateless signifie que notre EJB est sans état. Un tel composant ne maintient pas d'état conversationnel avec l'appelant du bean. Cela signifie qu'une même instance du bean peut servir plusieurs clients (ou appelants).


3/ Ecriture de notre premier test unitaire


Il ne nous reste plus qu'à créer le test unitaire qui va bien :


















 

Quelques explications maintenant. La première chose à faire est de créer un EJBContainer. Pour cela, il est nécessaire de créer une Map contenant toutes les propriétés correspondant à la configuration de notre conteneur :
  • EJBContainer.APP_NAME : Le nom de notre application, ici unittesting. Ce paramètre est optionnel ;
  • EJBContainer.MODULES : Les modules EJB à charger. Il peut s'agir de répertoires ou d'archives JAR référencées par des objets File. Dans notre cas ici, nous définissons un objet File pointant sur le répertoire "target/classes" (répertoire Maven de nos fichiers .class compilés). Ce paramètre semble obligatoire sinon il sera impossible pour le conteneur de localiser vos EJBs au démarrage. Dans ce cas, un message "ejb.embedded.no_modules_found" sera affiché ;
  • EJBContainer.PROVIDER : Le nom complet de la classe d'implémentation mise à disposition par le fournisseur du conteneur embarqué. C'est cette classe qu'il faut adapter suivant les fournisseurs. Ce paramètre est lui aussi optionnel car si glassfish-embedded-all-3.0.1.jar est dans le classpath, la classe d'implémentation sera automatiquement chargée.

Remarque sur JNDI :
EJBContainer.APP_NAME est un paramètre optionnel. S'il n'est pas paramétré alors les EJBs déployés seront accessibles via leur nom JNDI global (standardisé depuis Java EE 6). Ce dernier est construit de la manière suivante :
  • java:global/classes/NOM_DE_LA_CLASSE_EJB

Si, en revanche, ce paramètre est bien précisé, le nom global JNDI sera alors comme suit :
  • java:global/NOM_APPLICATION/NOM_DE_LA_CLASSE_EJB

Dans notre cas, puisque nous avons précisé le nom de l'application (unittesting), notre bean HelloService est accessible via l'URL JNDI suivante :
  • java:global/unittesting/HelloService

Il existe une autre manière de retrouver un EJB dans l'annuaire JNDI en suffixant l'URL par le nom de la classe d'implémentation :
  • java:global/unittesting/HelloService!fr.sydisnet.blog.embedded.hellocomponent.HelloService

Si maintenant, notre EJB implémente une interface IHelloService (cette dernière spécifiant la méthode String sayHello()), alors ce dernier sera accessible de deux manières :
  • java:global/unittesting/HelloService
  • java:global/unittesting/HelloService!fr.sydisnet.blog.embedded.hellocomponent.IHelloService : Dans ce dernier cas, le nom JNDI est suffixé par le nom complet de l'interface.

Parmi les trois paramètres précédents, seul EJBContainer.MODULES est obligatoire.Vous noterez que deux lignes ont été mises en commentaires :
  • La première configure l'emplacement d'un serveur Glassfish 3.0.1 déjà installé à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.installation.root. Cette propriété permet d'utiliser une installation existante afin de récupérer la configuration de l'instance du serveur. Cela est très utile pour pouvoir utiliser des ressources telles que des sources de données JDBC déjà paramétrées.
  • La deuxième est plus intéressante : Si aucune installation existante de Glassfish 3.0.1 n'est disponible, il est possible de spécifier au conteneur embarqué un fichier domain.xml à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.configuration.file. C'est dans ce fichier que de nouvelles sources de données peuvent être configurées. Je reviendrai ultérieurement sur ce fichier domain.xml lorque nous configurerons une source de données MySQL.

Une fois que notre EJBContainer est créé, il suffit de demander à ce dernier de récupérer le contexte de l'application (variable appContext).

Ensuite, tous les tests (méthodes annotées par @Test) sont exécutés. Le programme se termine lorsque la méthode annotée par @AfterClass est appelée. Dans cette méthode, nous fermons le contexte de l'application, (appContext.close()) et nous arrêtons le conteneur (container.close()).


Enfin, il ne nous reste plus qu'à exécuter notre test unitaire. Mais les choses ne se passent pas comme prévues. En effet, une erreur apparaît :
  • Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.095 sec <<< FAILURE!
    initializationError(fr.sydisnet.blog.embedded.hellocomponent.HelloServiceTest)  Time elapsed: 0.008 sec  <<< ERROR!
    java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/ejb/embeddable/EJBContainer
Que se passe t'il ? Et bien, disons qu'il s'agit d'un problème lié à Maven et à l'API Java EE 6 (merci à DominikDorn). En effet, dans nos dépendances (fichier pom.xml), nous avons déclaré l'API Java EE 6 mais cette dernière est incomplète et cela ne plaît pas à Maven. Pour contourner le problème, il faut déclarer une dépendance complète avant celle de l'API Java EE 6. Cela peut être fait comme suit :































Après correction, le test ne pose plus de problème. Toutefois, un message d'erreur apparaît : "ejb.embedded.location_not_exists". Ce dernier signifie que la propriété org.glassfish.ejb.embedded.glassfish.installation.root n'a pas été passée au conteneur. Puisque nous partons du principe que nous ne disposons d'aucune installation existante de Glassfish, nous n'avons pas à configurer ce paramètre.
Félicitations ! Vous venez d'exécuter votre premier test unitaire mettant en œuvre Glassfish Embedded :)


4/ Configuration d'une source de données MySQL

Comme cela a été évoqué précédemment, il suffit de paramétrer l'emplacement d'un fichier de configuration à l'aide de la propriété org.glassfish.ejb.embedded.glassfish.configuration.file. Ce fichier s'intitule domain.xml. Mais comment créer ce fichier ?

Déplacez-vous à l'emplacement local Maven de votre ordinateur où est stocké le fichier glassfish-embedded-all-ejb-3.0.1.jar. Après avoir copié et décompressé le fichier quelque part, vous trouverez le fichier domain.xml dans le sous répertoire org/glassfish/embed.

Il ne reste plus qu'à copier le fichier à l'emplacement de votre choix et à configurer la propriété org.glassfish.ejb.embedded.glassfish.configuration.file attendue par le conteneur.

Pour ma part, je n'ai pas configuré la propriété en question car j'ai opté pour une deuxième solution. En effet, par défaut, le fichier domain.xml se trouve dans le package org.glassfish.embed.

Cela signifie qu'en plaçant le répertoire domain.xml dans src/main/resources/org/glassfish/embed, nous n'avons plus besoin de configurer la propriété org.glassfish.ejb.embedded.glassfish.configuration.file en question.

Il ne reste plus qu'à configurer un pool de connexions, d'enregistrer ce dernier dans l'annuaire JNDI et de référencer la ressource en question au niveau du conteneur embarqué (cliquez sur l'image pour agrandir) :










































Une fois le fichier domain.xml en place, il ne faut pas oublier de configurer la dépendance au driver MySQL :







































Et voilà ! Notre source de données MySQL est configurée.


5/ Test de la source de données avec Glassfish Embedded


Pour tester notre conteneur embarqué et la source de données configurée, ajoutons le test unitaire comme suit :
















Normalement, vous devriez voir quelque part dans les logs la châine MySQL suivi du numéro de la version de votre SGBD favori :)



















































La suite !

Comme nous l'avons vu, il est facile d'utiliser "Glassfish Embedded" pour tester unitairement ses EJB. De plus, intégrer une source de données vers une base de données quelconque ne demande pas beaucoup plus de travail.

Puisqu'une DataSource MySQL est maintenant configurée, on peut facilement développer des entités JPA (bean annotés avec @Entity) manipulées par un EntityManager. Comment ? En configurant  une unité de persistance utilisant notre source de données "jdbc/test" dans un fichier "persistence.xml".

Cela fera l'objet d'un prochain article. [Note : Je pense faire un autre tutorial mettant en œuvre Arquillian].


Enfin, pour information, sachez que Glassfish Embedded ne s'arrête pas à de simples tests. En effet, ce dernier intègre un ORB client (gestionnaire d'objets distribués basé sur CORBA) ce qui signifie qu'il peut tout-à-fait se connecter à des EJB distants. En revanche, il ne permet pas d'héberger lui-même de tels composants.