Publié par
Il y a 6 années · 10 minutes · Java / JEE

Testabilité des EJB 3.1. Prêt pour du TDD ?

La testabilité est devenue un facteur à prendre en compte lors du choix d’un composant technique. Pour les EJB 3.0, il existait plusieurs manières de tester des services développés, Ejb3unit (figé depuis mi-2009) ou Arquillian (uniquement côté JBoss AS). Les EJB 3.1 offrent enfin une solution native, prête à l’emploi et simple à manipuler : l’EJB Container.

Cet article présente l’utilité de l’EJB Container et la manière de l’utiliser ; il s’appuie pour cela sur l’écriture d’un service et d’une suite de tests exécutables sur Glassfish 3 et sur JBoss 6.

L’EJB Container en théorie

Les EJB 3.1 sont spécifiés dans la JSR 318 dont la version finale a été validée en décembre 2009. Outre les spécifications liées au cœur même des EJB (Session Bean Stateless, Stateful, Message-Driven Bean, Timer Service, Transactions, etc) on trouve un chapitre dédié à un tout nouveau composant : l’EJB Container. Voici un résumé des fonctionnalités et obligations spécifiées :

  • Supporter le sous-ensemble EJB Lite de l’API EJB 3.1 dans un environnement de conteneur embarqué ;
  • Permettre à un code client (un programme Java standard par exemple) d’instancier un EJB Container qui s’exécute dans sa propre JVM avec son propre classloader ;
  • Définir une API permettant de démarrer le conteneur et de récupérer les Enterprise Beans ;
  • Fournir un environnement qui supporte les services de base tels que l’injection de dépendances, l’accès à l’environnement, la gestion des transactions, etc.

Pour rappel, le sous-ensemble EJB Lite est composé :

  • De la prise en charge des Session Beans Stateless, Stateful, et Singleton ;
  • Uniquement des EJB avec interface Local ou sans interface ;
  • De la prise en charge des Interceptors ;
  • Du support de la sécurité ;
  • Du support des transactions.

La JSR 318 n’étant qu’une spécification, l’implémentation de ces fonctionnalités repose sur les éditeurs de serveurs d’applications.

L’EJB Container en pratique

L’EJB Container est le moteur qui va nous permettre de simuler l’exécution de nos EJB comme si nous étions dans le « vrai » conteneur EJB de notre serveur d’applications.

Un cas simple : Hello World !

Pour démarrer, nous allons développer un simple EJB HelloWorldService. Le test consistera à vérifier que la méthode sayHelloWorld() de HelloWorldService renvoie bien « Hello World ».

Voici tout d’abord notre bean HelloWorldService. Sa responsabilité est de retourner la chaîne de caractères « Hello World » lors de l’appel de la méthode sayHelloWorld.

@Stateless
@Local
public class HelloWorldService implements IHelloWorldService {

    public String sayHelloWorld() {
        return "Hello World";
    }
}

Pour tester uniquement cet EJB nous créons un test JUnit qui se charge d’instancier l’EJBContainer et de récupérer notre Bean. Le seul et unique test sera de vérifier que l’appel à la méthode sayHelloWorld retourne bien « Hello World ».

public class HelloWorldServiceTest {

    private static EJBContainer ejbContainer;

    private static IHelloWorldService helloWorldService;

    /***
     * Méthode d'initialisation appelée une seule fois lors de l'exécution
     * des tests de HelloServiceTest.
     * C'est l'endroit idéal pour démarrer l'EJBContainer et récupérer
     * les EJB à tester.
     * @throws NamingException
     */
    @BeforeClass
    public static void init() throws NamingException {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(EJBContainer.MODULES, new File("target/classes.ext"));
        ejbContainer = EJBContainer.createEJBContainer(properties);
        Context ctx = ejbContainer.getContext();

        // le nom JNDI d'un EJB dépend du serveur d'applications utilisé :
        // jboss     : "HelloWorldService/local"
        // glassfish : "java:global/classes.ext/HelloWorldService"
        String helloWorldServiceName = HelloWorldService.class.getSimpleName();
        helloWorldServiceName = isJbossContainer() ? helloWorldServiceName + "/local" : "java:global/classes.ext/" + helloWorldServiceName;
        helloWorldService = (IHelloWorldService) ctx.lookup(helloWorldServiceName);
    }

    /***
     * Méthode de test qui vérifie que nous avons bien récupéré l'EJB
     * HelloWorldService et qu'il est fonctionnel
     */
    @Test
    public void should_say_hello_world() {
        Assert.assertEquals("Hello World", helloWorldService.sayHelloWorld());
    }

    /***
     * Méthode de nettoyage appelée une seule fois après l'exécution de
     * l'ensemble des tests unitaires de HelloServiceTest.
     * C'est l'endroit idéal pour fermer le contexte JNDI et l'EJBContainer.
     * Un bug de JBoss nous contraint à ne pas appeler les méthodes close()
     * sur context et container.
     * @throws NamingException
     */
    @AfterClass
    public static void cleanup() throws NamingException {
        ejbContainer.close();
    }

    private static boolean isJbossContainer() {
        return System.getProperty("jboss.home") != null;
    }
}

Comme convenu par les spécifications, un EJBContainer est instancié grâce à la méthode EJBContainer.createEJBContainer() et fermé par la méthode EJBContainer.close().

Lors de la création de l’EJBContainer il faut préciser le répertoire des classes compilées. Habituellement lorsqu’on utilise Maven, ce répertoire est target/classes.

Malheureusement JBoss 6.0.0 Final contient un bug qui nous oblige à compiler nos EJB dans un dossier suffixé par 4 caractères (par exemple : target/classes devient target/classes.ext). En effet, lors de l’exécution du déploiement du conteneur, les 4 derniers caractères seront tronqués. La prochaine version de JBoss (v6.0.1) corrigera cette anomalie.

Une fois le conteneur récupéré, il suffit de passer par le Context JNDI pour rapatrier ses beans mais attention : le nom JNDI des EJB est différent selon le serveur d’applications utilisé.

Nous avons défini deux profils Maven dans le pom.xml permettant de passer d’une exécution avec Glassfish à une exécution avec JBoss. Pour choisir, il suffit d’ajouter le paramètre -Pglassfish ou le paramètre -Pjboss.

Pour exécuter les tests sur Glassfish il suffit d’exécuter la commande suivante :

mvn test -Pglassfish

Pour exécuter les tests sur JBoss il suffit d’exécuter la commande suivante :

mvn test -Pjboss

Le temps d’exécution total est très différent entre les deux conteneurs :

  • Glassfish 3 : 6 secondes
  • JBoss 6 Final : 61 secondes

Un cas plus complexe : EJB 3.1 + JPA 2

Pour aller plus loin, nous allons développer un service permettant d’enregistrer des personnes dans une base de données.

Pour manipuler nos entités Person nous créons PersonService qui est un EJB Session Bean Stateless utilisant l’EntityManager de JPA 2.0.

@Stateless
@Local
/**
 * Service permettant la gestion des entités Person avec la base de données
 * Seule la méthode de création est implémentée.
 */
public class PersonService implements IPersonService {

    @PersistenceContext
    EntityManager em;

    /**
     * Crée une nouvelle Person dans la base de données.
     * Méthode transactionnelle :
     *  @TransactionAttribute(TransactionAttributeType.REQUIRED) est implicite
     * @param person une instance de Person
     * @return L'instance de personne persistée, champ Id initialisé
     */
    @Override
    public Person create(Person person) {
        em.persist(person);
        return person;
    }
}

En nous arrêtant là, et à 2 fichiers de configuration près, notre EJB est packageable en JAR et déployable sur les serveurs d’applications JBoss et Glassfish :

  • ejb-jar.xml contient la liste de vos EJB. Ce fichier est portable et sera le même quel que soit le serveur d’applications ;
  • persistence.xml contient les paramètres de connexion à la base de donnée. La configuration de ce fichier est fortement liée au choix de serveur d’applications. Pour JBoss, Hibernate sera privilégié comme Persistence Provider (persistence.xml), alors que pour Glassfish ce sera plutôt Eclipse Link (persistence.xml).

Écriture et exécution du test

Comme précédemment, nous utilisons un test JUnit pour valider le comportement de PersonService. Pour plus de clarté, l’exemple ci-dessous ne contient pas les méthodes d’initialisation et de fermeture de l’EJBContainer :

public class PersonServiceTest {

    private static IPersonService personService;

    /***
     <strong> Méthode de test qui vérifie la création d'un objet Person dans la
     </strong> base de données. L'instance obtient un identifiant après avoir été
     * persistée par l'EntityManager.
     */
    @Test
    public void should_create_a_person() {
        Person person = new Person("Erich", "Gamma");
        Person createdPerson = personService.create(person);
        assertNotNull(createdPerson.getId());
    }
}

Regardons maintenant comment se comporte notre EJB Container en rajoutant ce nouvel EJB. Tout d’abord, voici les temps d’exécution mis à jour :

  • Glassfish 3 : 12 secondes
  • JBoss 6 Final : 67 secondes

Le temps d’exécution est toujours beaucoup plus faible en utilisant Glassfish 3 que JBoss 6.

Glassfish a mis 2 fois plus de temps à démarrer à cause des phases d’initialisation dues à l’utilisation de JPA 2.0 (entity manager, création de la base de données, création des tables, mapping des entités, etc). JBoss a quant à lui mis quelques secondes de plus que précédemment, il charge un conteneur entier et ne semble pas vouloir jouer la carte de la légèreté comme le fait Glassfish.

Ce temps total d’exécution est voué à augmenter tout au long du projet, au fur et à mesure des nouvelles entités, des nouveaux services. Une chose est sûre, des outils comme Infinitest — lançant en boucle les tests des EJB — sont à proscrire. Il n’est pas concevable de démarrer des conteneurs qui mettent plus d’une minute à s’initialiser!

Les limites de l’EJB Container

En conclusion, l’EJB Container est un vrai plus dans la testabilité des EJB 3.1. Sa simplicité d’utilisation permet de se reposer facilement sur le conteneur dans nos tests ; il permet même d’envisager le développement dirigé par les tests (TDD). Malheureusement, sa trop grande durée d’initialisation n’encouragera pas à lancer les tests toutes les 5 secondes.

Ce qu’il faut retenir c’est qu’après le démarrage de l’EJB Container, les tests sont exécutés à vitesse grand V. Ils feront partie des tests d’intégration et nous permettront toujours de déceler les régressions et bugs potentiels.

Nous n’avons pas exploré dans cet article les limites techniques dues au sous-ensemble réduit des EJB Lite. Si vous pensez avoir des cas dans lesquels l’EJB Container ne peut pas vous aider, laissez-nous un commentaire :-).

Code source

L’intégralité de l’exemple est disponible sur Github. Maven et JBoss 6.0. sont nécessaires à son bon fonctionnement.

Pour JBoss, une variable d’environnement JBOSS_HOME pointant vers le répertoire d’installation est indispensable.

Pour en savoir plus sur les EJB 3.1

Le livre Beginning Java™ EE 6 Platform with GlassFish™ 3: From Novice to Professional d’Antonio Goncalves permet d’approfondir les possibilités offertes par EJB 3.1 et Java EE 6. Plusieurs chapitres sont dédiés aux EJB 3.1 (Session Beans, Transactions, Timer Service, etc). Les autres chapitres portent sur JPA 2.0, JSF 2.0 ainsi que sur l’écriture de Web Services SOAP et REST. Ce livre est un guide qui se lit vite et permet de mettre rapidement le pied à l’étrier.

Annexe

Si vous exécutez les tests avec la version 6.0.0 de JBoss AS, attendez-vous à une erreur lors de la fermeture de l’EJB Container. En effet, une anomalie lève une stacktrace lors de l’appel à la méthode ejbContainer.close().

21:04:59,128 ERROR [AOPClassLoaderDeployer] Error during undeploy: vfs:///Users/juliensmadja/Documents/workspace_fluxx/EJBContainer/target/classes.ext/: java.lang.NullPointerException
	at org.jboss.aop.asintegration.jboss5.AOPClassLoaderInitializer.unregisterLoaders(AOPClassLoaderInitializer.java:54) [jboss-aop-asintegration-mc.jar:2.2.1.GA]
	at org.jboss.aop.asintegration.jboss5.AOPClassLoaderDeployer.internalUndeploy(AOPClassLoaderDeployer.java:77) [jboss-aop-deployers.jar:2.2.1.GA]
	at org.jboss.deployers.spi.deployer.helpers.AbstractRealDeployer.undeploy(AbstractRealDeployer.java:117) [jboss-deployers-spi.jar:2.2.0.GA]
	at org.jboss.deployers.plugins.deployers.DeployerWrapper.undeploy(DeployerWrapper.java:204) [jboss-deployers-impl.jar:2.2.0.GA]
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doUndeploy(DeployersImpl.java:1862) [jboss-deployers-impl.jar:2.2.0.GA]
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doUninstallParentLast(DeployersImpl.java:1769) [jboss-deployers-impl.jar:2.2.0.GA]
	at org.jboss.deployers.plugins.deployers.DeployersImpl.uninstall(DeployersImpl.java:1724) [jboss-deployers-impl.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractControllerContext.uninstall(AbstractControllerContext.java:385) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.uninstall(AbstractController.java:2078) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.uninstallContext(AbstractController.java:1624) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.uninstallContext(AbstractController.java:1472) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.uninstall(AbstractController.java:756) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.uninstall(AbstractController.java:669) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.dependency.plugins.AbstractController.shutdown(AbstractController.java:270) [jboss-dependency.jar:2.2.0.GA]
	at org.jboss.bootstrap.impl.mc.server.AbstractMCServerBase.shutdownKernelAndDeployer(AbstractMCServerBase.java:202) [jboss-bootstrap-impl-mc.jar:2.1.0-alpha-5]
	at org.jboss.bootstrap.impl.mc.server.AbstractMCServerBase.doShutdown(AbstractMCServerBase.java:160) [jboss-bootstrap-impl-mc.jar:2.1.0-alpha-5]
	at org.jboss.bootstrap.impl.base.server.AbstractServer.shutdown(AbstractServer.java:304) [jboss-bootstrap-impl-base.jar:2.1.0-alpha-5]
	at org.jboss.ejb3.embedded.sub.JBossSubmersibleEJBContainer.close(JBossSubmersibleEJBContainer.java:76) [jboss-ejb3-embedded-sub.jar:1.0.0-alpha-4]
	at com.xebia.ejbcontainer.service.HelloWorldServiceTest.cleanup(HelloWorldServiceTest.java:57)
Julien Smadja
Julien Smadja est consultant manager chez Xebia où il intervient notamment sur des projets NodeJS et AngularJS 2. Ses 10 ans d'expérience ont principalement été axées sur le développement d'applications Java, la qualité et la testabilité.

7 réflexions au sujet de « Testabilité des EJB 3.1. Prêt pour du TDD ? »

  1. Publié par Alexis MP, Il y a 6 années

    Merci pour ce billet. OpenEJB est particulièrement bon sur ce domaine des tests. La moyenne des temps de tests sur différents conteneurs EJB devrait baisser sensiblement si tu les intègres dans tes essais.

  2. Publié par Julien Smadja, Il y a 6 années

    Merci pour vos commentaires

    @Igor & @Alexis MP
    D’après vos différentes expériences avec Arquillian et/ou OpenEJB, voyez-vous un intérêt à délaisser ces solutions au profit de l’EJBContainer des EJB 3.1 ?

    Quelles sont les limitations que vous avez pu rencontrer ?

  3. Publié par Antonio Goncalves, Il y a 6 années

    On peut simplifier d’autant plus le code que l’interface IHelloWorldService est optionnelle.

    Je n’ai pas testé avec la toute dernière version de JBoss, mais avec Java EE 6 les noms JNDI ont été standardisé (http://blogs.sun.com/kensaks/entry/application_specified_portable_jndi_names). Donc, le nom JNDI de l’EJB devrait être portable d’un serveur à l’autre. Essai le nom :

    java:global/classes.ext/HelloWorldService
    ou
    java:global/classes.ext/HelloWorldService!mon.package.IHelloWorldService

    Avec Alexis nous avions fait tourner des tests sur GlassFish 3.0.1 et JBoss 6 béta (pas la dernière version) et le nom JNDI était le même. Jette un oeil à : http://kenai.com/projects/beginningee6/sources/src/show/tutorial/trunk/Completed/Demo06-EJBTest

    Sinon, pas besoin d’ejb-jar.xml, il est optionnel dans la plupart des cas.

    Au fait, très bon choix pour le livre ;o)

  4. Publié par BARON Mickael, Il y a 6 années

    Je confirme, OpenEJB est très bon et la communauté est très réactive.

    Il permet également d’utiliser un EJB sous la forme d’un Web Service (@WebService).

  5. Publié par Laurent, Il y a 6 années

    La limitation actuelle que je vois sur Arquillian est la non intégration du framework DBUnit pour les tests JPA. En effet, lorsque l’on aborde des tests mettant en oeuvre des données persistantes, il est absolument nécessaire de pouvoir maitriser l’état initial de la base avant chaque test case. Il faut noter que l’équipe Arquillian a prévu de travailler sur l’intégration de ce framework, mais ce n’est pas encore fait. Pour l’avoir testé, il y a des problèmes de classloader entre le framework DBUnit et Arquillian et pour le moment, cela ne semble pouvoir marcher par exemple dans un contexte hsqldb en mémoire avec création du schéma de la BD et peuplement avant chaque test. Il faut reposer sur une base existante (au sens schéma). Donc pas dans le contexte de TU.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *