Actuellement j’interviens sur un projet dans lequel nous utilisons EJB 3.0 et WebLogic.
Comme j’aime bien avoir du code de qualité, j’ai commencé à réaliser des tests unitaires en parallèle de mes développements.
EJB 3.0 simplifie grandement la tâche cependant un bean entreprise doit quand même être déployé avant de pouvoir être testé dans sa globalité. Certains diront que la logique applicative doit être dans un POJO plutôt que dans le bean lui-même. Il n’empêche, pour s’assurer que le bean fonctionne correctement, il doit être déployé dans un conteneur.
Sous JUnit j’ai été confronté rapidement à plusieurs problématiques :
- Comment tester un bean qui fait appel à un autre bean via un lookup ?
- Comment tester qu’un bean s’initialise correctement lors d’un lookup ou d’une injection d’EJB ?
- Comment s’assurer qu’un MDB est bien configuré ?
- Comment s’assurer qu’un bean réalise correctement une publication JMS ?
Dans ma quête de pouvoir réaliser des tests unitaires poussés sous JUnit sans avoir à déployer mes beans vers un serveur spécifique, je suis finalement tombé sur OpenEJB.
Il s’agit non seulement d’un serveur d’EJB mais aussi d’un conteneur intégré d’EJB.
Via OpenEJB et JUnit je m’assure ainsi que les beans déployés plus tard sous Weblogic réagissent bien comme je l’attends.
OpenEJB permet aussi de se dégager des problématiques de déploiement sous Weblogic et de se consacrer intégralement au développement des fonctionnalités propre à chaque bean entreprise.
Récupération d’OpenEJB
Sous maven il suffit d’ajouter la dépendance suivante :
<dependency>
<groupId>org.apache.openejb</groupId>
<artifactId>openejb-core</artifactId>
<version>4.0.0-beta-1</version>
<scope>test</scope>
</dependency>
Détection des beans
Le conteneur OpenEJB parcourt le classpath afin de récupérer les beans annotés :
@Stateless
@Stateful
@MessageDriven
Pour améliorer la rapidité du parcours il est recommandé de placer un fichier ejb-jar.xml
(vide ou non) dans :
src/main/resources/META-INF/
Il est aussi possible de restreindre le parcours du classpath au projet via une propriété OpenEJB :
openejb.deployments.classpath.include = .*oxiane-openejb-demo.*
Définition des propriétés JNDI OpenEJB
Les propriétés JNDI OpenEJB sont définies soit par l’intermédiaire d’un fichier jndi.properties
placé dans src/test/resources/
soit en intégrant directement la définition des propriétés dans le cas de test :
# Initialise la fabrique de contexte initial.
java.naming.factory.initial = org.apache.openejb.client.LocalInitialContextFactory
# Définit le format des noms JNDI (exemple: HelloWorldBean/Local).
openejb.jndiname.format = {ejbName}/{interfaceType.annotationName}
# Flags pour tracer toutes les actions liées au démarrage du conteneur.
openejb.descriptors.output = true
openejb.validation.output.level = verbose
ou
...
Properties p = new Properties();
// Initialise la fabrique de contexte initial.
p.put("java.naming.factory.initial", "org.apache.openejb.client.LocalInitialContextFactory");
// Définit le format des noms JNDI (exemple: HelloWorldBean/Local).
p.put("openejb.jndiname.format", "{ejbName}/{interfaceType.annotationName}");
// Flags pour tracer toutes les actions liées au démarrage du conteneur.
p.put("openejb.descriptors.output", "true");
p.put("openejb.validation.output.level", "verbose");
...
Définition de ressources OpenEJB
Si des ressources sont nécessaires lors des test (broker JMS, topic JMS, etc), il faut créer le fichier conf/openejb.xml
.
Pour les exemples à suivre, j’ai besoin d’un broker JMS, d’une connection factory, d’un conteneur de MDB et d’une topic « PublisherTopic ».
<openejb>
<!-- Broker JMS -->
<Resource id="JMSResourceAdapter" type="ActiveMQResourceAdapter">
BrokerXmlConfig = broker:(tcp://localhost:61616)
ServerUrl = tcp://localhost:61616
</Resource>
<!-- Connection factory JMS -->
<Resource id="JMSConnectionFactory" type="javax.jms.ConnectionFactory">
ResourceAdapter JMSResourceAdapter
</Resource>
<!-- Conteneur du MDB -->
<Container id="MdbContainer" ctype="MESSAGE">
ResourceAdapter JMSResourceAdapter
</Container>
<!-- Topic JMS -->
<Resource id="PublisherTopic" type="javax.jms.Topic" />
</openejb>
Test JUnit avec lookup
L’exemple ci-dessous illustre la manière de mettre en oeuvre un cas de test JUnit portant sur un EJB récupéré via un lookup.
Code du bean testé, HelloWorldBean:
public interface IHelloWorldBean {
public String helloWorld();
}
@Stateless
@Local(IHelloWorldBean.class)
@Remote(IHelloWorldBean.class)
public class HelloWorldBean implements IHelloWorldBean{
@Override
public String helloWorld() {
return "Hello world !";
}
}
Code du cas de test :
@LocalClient
public class HelloWorldBeanTest {
private IHelloWorldBean helloWorld;
@Test
public void helloWorldTest() throws NamingException {
final String result = helloWorld.helloWorld();
assertNotNull(result);
assertEquals("Hello world !", result);
}
@Before
public void setUp() throws NamingException {
final Properties p = new Properties();
p.setProperty("java.naming.factory.initial", "org.apache.openejb.client.LocalInitialContextFactory");
p.setProperty("openejb.jndiname.format", "{ejbName}/{interfaceType.annotationName}");
p.setProperty("openejb.deployments.classpath.include", ".*oxiane-openejb-demo.*");
helloWorld = ((IHelloWorldBean) new InitialContext(p).lookup("HelloWorldBean/Local"));
}
}
Un simple new InitialContext()
, avec en paramètres les propriétés souhaitées, suffit pour démarrer le conteneur intégré OpenEJB.
Test JUnit avec injection d’EJB
OpenEJB offre la possibilité d’injecter des EJB directement dans le cas de test JUnit.
En modifiant quelque peu le cas de test HelloWorldBeanTest on obtient ceci :
public class HelloWorldBeanInjectionTest {
private EJBContainer ejbContainer;
@EJB
private IHelloWorldBean helloWorld;
@Test
public void helloWorldTest() throws NamingException {
final String result = helloWorld.helloWorld();
assertNotNull(result);
assertEquals(result, "Hello world !");
}
@Before
public void setUp() throws NamingException {
final Properties p = new Properties();
p.setProperty("java.naming.factory.initial",
"org.apache.openejb.client.LocalInitialContextFactory");
p.setProperty("openejb.validation.output.level", "verbose");
p.setProperty("openejb.deployments.classpath.include", ".*oxiane-openejb-demo.*");
ejbContainer = EJBContainer.createEJBContainer(p);
//Injection des ressources.
ejbContainer.getContext().bind("inject", this);
}
@After
public void tearDown() {
ejbContainer.close();
}
}
Le conteneur EJB est ici créé manuellement. Cette manière de procéder permet ensuite de récupérer son contexte et de réaliser l’injection de l’EJB HelloWorldBean.
Test JUnit d’un MDB
Pour ce troisième exemple j’utilise deux beans : SimpleMDB et PublisherBean.
SimpleMDB est un MDB souscrivant à la topic JMS « SimpleMDBTopic ». Pour chaque message reçu, il transmet le corps textuel du message à PublisherBean.
@MessageDriven(name = "SimpleMDB", activationConfig = {
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
@ActivationConfigProperty(propertyName = "destination", propertyValue = "SimpleMDBTopic"),
@ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge")})
public class SimpleMDB implements MessageListener {
private static Logger logger = Logger.getLogger(SimpleMDB.class);
@EJB
private IPublisherBean publisherBean;
@Override
public void onMessage(final Message jmsMessage) {
try {
publisherBean.publish(((TextMessage) jmsMessage).getText());
} catch (final Exception e) {
if (logger.isEnabledFor(Level.ERROR)) {
logger.error("Error while handling message", e);
}
}
}
}
PublisherBean est un bean stateless publiant un message sur la topic JMS « PublisherTopic ».
public interface IPublisherBean {
public void publish(String news);
}
@Stateless
@Local(IPublisherBean.class)
@Remote(IPublisherBean.class)
public class PublisherBean implements IPublisherBean {
...
@Resource(name = "JMSConnectionFactory")
private TopicConnectionFactory connectionFactory;
@Resource(name = "PublisherTopic")
private Topic publisherTopic;
@Override
public void publish(final String message) {
try {
// Traitment du message ...
if (logger.isDebugEnabled()) {
logger.debug("Publisher: " + message);
}
// Publication JMS
publisher.send(session.createTextMessage(message));
} catch (final JMSException e) {
if (logger.isEnabledFor(Level.ERROR)) {
logger.error("Publishing error", e);
}
}
}
@PostConstruct
public void postConstruct() throws JMSException, NamingException {
// Initialisation session, connection et publisher JMS
...
}
...
}
La topic « SimpleMDBTopic » est créée par le conteneur OpenEJB lors de l’activation de SimpleMDB.
La connection factory JMS et la topic « PublisherTopic » sont des ressources OpenEJB définies dans conf/openejb.xml
.
Le cas de test (SimpleMDBTest) doit vérifié que l’enchaînement des traitements suivants fonctionne correctement :
- – Lecture d’un message JMS par SimpleMDB sur la topic « SimpleMDBTopic »,
- – Transmission du message de SimpleMDB à PublisherBean,
- – Publication d’un message JMS par PublisherBean sur la topic « PublisherTopic »,
- – Le message émis sur « SimpleMDBTopic » est ensuite reçu sur « PublisherTopic ».
Il vérifie également que les injections des ressources et des EJBs se réalisent sans accroc.
SimpleMDBTest :
@LocalClient
public class SimpleMDBTest implements MessageListener {
/** Ressources JMS */
@Resource(name = "PublisherTopic")
private Topic publisherTopic;
@Resource(name = "SimpleMDBTopic")
private Topic simpleMDBTopic;
@Resource(name = "JMSConnectionFactory")
private TopicConnectionFactory connectionFactory;
...
}
SimpleMDBTest implémente MessageListener de manière à pouvoir souscrire à la topic de PublisherBean.
L’injection des ressources dans SimpleMDBTest est déclenché lors du démarrage du conteneur lors du setUp.
private void setUpOpenEJBContainer() throws NamingException {
final Properties p = new Properties();
p.setProperty("java.naming.factory.initial",
"org.apache.openejb.client.LocalInitialContextFactory");
p.setProperty("openejb.deployments.classpath.include", ".*oxiane-openejb-demo.*");
ejbContainer = EJBContainer.createEJBContainer(p);
ejbContainer.getContext().bind("inject", this);
}
Les subscriber et publisher JMS pour la réception et l’envoi de messages sont également paramètrés lors du setUp.
...
/** Topic subscriber lié à PublisherTopic. */
private TopicSubscriber subscriber;
/** Topic connection. */
private TopicConnection connection;
/** Topic publisher lié à SimpleMDBTopic. */
private TopicPublisher publisher;
/** Topic Session. */
private TopicSession session;
...
private void setUpJMS() throws JMSException {
connection = connectionFactory.createTopicConnection();
session = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
// Souscripteur lié à la topic de PublisherBean.
subscriber = session.createSubscriber(publisherTopic);
subscriber.setMessageListener(this);
// Publicateur lié à la topic du MDB.
publisher = session.createPublisher(simpleMDBTopic);
connection.start();
}
...
Deux verrous, startLatch et doneLatch sont utilisées par le thread principal pour se synchroniser avec les threads secondaires crées lors de chaque appel de la méthode onMessage().
...
/** Verrous de synchronisation */
private CountDownLatch doneLatch;
private CountDownLatch startLatch;
/** Liste des messages envoyes */
private static final List MESSAGES = Arrays.asList("OpenEJB rocks.",
"Next post about Magritte3.", "Programming do: [:it | it with: #fun ]");
/** Compte le nombre de messages envoyes par {@link PublisherBean} */
private int received;
...
private void setUpLatches() {
startLatch = new CountDownLatch(1);
doneLatch = new CountDownLatch(MESSAGES.size());
received = 0;
}
...
La méthode onMessage() réceptionne le message et vérifie que le message reçu sur « PublisherTopic » fait partie des messages émis sur « SimpleMDBTopic ».
...
@Override
public void onMessage(final Message message) {
try {
// Attends le déblocage du thread principal pour déclencher le traitement.
startLatch.await();
final String messageContent = ((TextMessage) message).getText();
assertNotNull(messageContent);
assertTrue(MESSAGES.contains(messageContent));
received++;
// Signale au thread principal que le traitement est terminé.
doneLatch.countDown();
} catch (final JMSException e) {
e.printStackTrace();
fail();
} catch (final InterruptedException e) {
e.printStackTrace();
fail();
}
}
...
Le lancement du test est en lui-même assez simple.
...
@Test
public void mdbTest() throws JMSException, InterruptedException {
//Emission des messages sur SimpleMDBTopic
for (final String message : MESSAGES) {
publisher.send(session.createTextMessage(message));
}
// Débloque les threads traitant les messages envoyes par PublisherBean.
startLatch.countDown();
// Attends la fin d'execution des threads.
doneLatch.await();
//Nombre de messages émis = nombre de messages reçus.
assertEquals(MESSAGES.size(), received);
}
...
Test d’un MDB EJB 3.0 sous JUnit, done !
Bilan
Le conteneur OpenEJB embarqué dans les tests JUnit permet d’améliorer significativement la qualité du code produit. Le fait de pouvoir tester le comportement d’un EJB au sein d’un conteneur d’EJB permet d’éliminer un bon paquet de bugs avant de le déployer sur le serveur (Weblogic dans mon cas).
Mettre en place des tests JUnit sous OpenEJB est relativement facile, la documentation OpenEJB est concise et va à l’essentiel.
De plus, Un grand nombre d’exemples, téléchargeables ici, illustrent bien les différents cas d’utilisation.