We apologize for untranslated text, you can use the Google Translation button to get an automatic translation of the web page in the language of your choice.

JCMS : How to write unit tests with JCMS (french)

Afin de vous assurer du bon fonctionnement de vos développements Java, il est recommandé de développer des tests unitaires. En réalisant ces tests, vous validez que votre code répond aux spécifications fonctionnelles et vous limitez l'apparition de dysfonctionnements lors de vos futures modifications (tests de non régression).

Cet article présente l'utilisation de l'outil de tests unitaires JUnit au sein de JCMS. La lecture de cet article nécessite une connaissance préalable de JUnit.

1. Principes

1.1 JCMS et JUnit

Les tests unitaires JUnit ne peuvent pas être exécutés depuis un serveur d'application, ils n'ont donc pas accès à l'API disponible dans une instance de JCMS en fonctionnement.

Pour répondre à cette problématique, Jalios fournit une surcouche à l'API JUnit pour qu'une instance de test de JCMS soit démarrée avant l'exécution des tests unitaires.
Afin d'utiliser cette surcouche, les classes de tests doivent dériver de la classe JcmsTestCase.

1.2 La classe JcmsTestCase

En dérivant de la classe com.jalios.jcms.test.JcmsTestCase, les tests unitaires bénéficient de deux points essentiels :

  1. Un environnement JCMS complet lors de leur exécution.
  2. Une API vous permettant de simplifier le développement des tests.

1.2.1 Environnement JCMS

Lors de l'exécution de vos tests, tout l'environnement JCMS habituellement disponible dans un serveur d'application est disponible automatiquement.

Afin de ne pas altérer les fichiers de données de la webapp lors des tests unitaires, les fichiers suivants sont recopiés comme suit :

  • store.xml est copié vers store.xml.junit
  • custom.prop est copié vers custom.prop.junit

Ces copies sont alors utilisées pour démarrer la webapp : Le singleton Channel est initialisé et tous les processus de JCMS sont démarrés.

Attention ! D'autres fichiers sont susceptibles d'être modifiés lors de l'exécution des tests unitaires. Vérifiez que les modifications de ces fichiers ne soient pas gênantes dans votre environnement. Voici notamment quelques fichiers ou dossiers qui pourraient être impactés selon la nature de vos tests :

  • upload/*
  • archives/*
  • WEB-INF/data/lucene/*

Attention ! Lors de l'exécution de vos tests unitaires, des mails pourraient être envoyés (notification, alerte de workflow, etc.) Il est recommandé de désactiver le serveur de mail via l'éditeur de propriétés.

1.2.2 L'API : variables membres et méthodes

Les variables membres suivantes sont disponibles dans vos méthodes de tests :

  • Channel channel : le singleton du Channel
  • Member admin : l'administrateur par défaut du site (cf. propriété channel.default-admin dans custom.prop)
  • Workspace defaultWorkspace : le workspace par défaut du site (cf. propriété channel.default-workspace dans custom.prop)
  • Group defaultGroup : le groupe par défaut du site (cf. propriété channel.default-group dans custom.prop)

Des méthodes viennent compléter l'API de JUnit, notamment :

  • Pour la vérification des ControllerStatus retournés par les DataController
    • assertStatusOK(ControllerStatus )
    • assertStatusForbidden(ControllerStatus )
    • assertStatusHasFailed(ControllerStatus )
  • Pour la comparaison de tableau, de Collection
    • assertSameContent(Object[] , Object[] )
    • assertNotSameContent(Object[] , Object[] )
    • assertSameContent(Set , Set )
    • assertNotSameContent(Set , Set )
    • assertSameContent(List , List )
    • assertNotSameContent(List , List )
    • assertSameContent(Map , Map )
    • assertNotSameContent(Map , Map )
  • Pour la manipulation des sessions Hibernate (cf section 4.6)
    • beginTransaction()
    • commitTransaction()

Consultez la JavaDoc pour plus d'informations sur l'API fournie par la classe JcmsTestCase.

2. Configuration Eclipse

  • Ajoutez la librairie JUnit 4 dans les librairies de votre projet.
    Faites un clic droit sur le projet, puis choisissez Properties > Section Java Build Path > Onglet Libraries > Bouton Add Library > JUnit > JUnit 4
  • Référencez également la librairie tools.jar de votre JDK actuelle dans les librairies du projet.
    Pour cela utilisez le bouton Add External Jar et sélectionnez le jar de la JDK.
Configuration des librairies dans Eclipse

Fig. 1. Configuration des librairies dans Eclipse

3. Exemple

Dans cet exemple, nous allons développer un DataController qui doit vérifier que le titre a été renseigné dans toutes les langues du site (et pas uniquement dans la langue du contenu).

On supposera que tous les développements sont réalisés dans un modules DemoPlugin.
Le module et ses sources sont téléchargeables via le lien suivant : Module de démonstration (avec tests unitaires).

3.1 Le test unitaire

Comme il est recommandé dans la méthodologie Test Driven Development, on commence par développer le test unitaire.

  1. Pour ce test, utilisez une webapp configurée en français et anglais avec le type de contenu SmallNews fourni en standard.
  2. Créez une classe TitleControllerTest qui dérive de la classe com.jalios.jcms.test.JcmsTestCase.
  3. Créez une nouvelle methode de test : testCheckIntegrity()
  4. Premier test : vérifier qu'aucun message n'est renvoyé si tous les titres sont présents.
  5. Second test : vérifier qu'un message est renvoyé si l'un des titres est manquant.

WEB-INF/classes/org/demo/jcmsplugin/demo/TitleControllerTest.java :

import generated.SmallNews;

import com.jalios.jcms.ControllerStatus;
import com.jalios.jcms.test.JcmsTestCase;
import com.jalios.util.JProperties;

public class TitleControllerTest extends JcmsTestCase {

public void testCheckIntegrity() {
// Everything OK
{
SmallNews sm = new SmallNews();
sm.setTitle("en", "My title");
sm.setTitle("fr", "Mon titre");
sm.setContent("whatever");
sm.setAuthor(admin);

assertStatusOK(sm.checkIntegrity());
}

// Title is missing in French
{
SmallNews sm = new SmallNews();
sm.setTitle("en", "My title");
sm.setContent("whatever");
sm.setAuthor(admin);

ControllerStatus status = sm.checkIntegrity();
// Check failure with proper message
assertStatusHasFailed(status);
assertEquals("jcmsplugin.demo.title-missing", status.getProp());
}
}
}

3.2 Le DataController

Il s'agit ici de développer un DataController, si vous n'êtes pas familier avec cette API consultez l'article : JCMS 5.7 : Développer avec DCM et les DataController

  1. Créez une classe TitleController dérivant de BasicDataController ;
  2. Référencez cette classe dans le fichier plugin.xml de votre module ;
  3. Implémentez la méthode checkIntegrity(...) ;
  4. Testez la présence des titres dans toutes les langues ;
  5. Le cas échéant renvoyez un status d'erreur ;

WEB-INF/plugins/DemoPlugin/plugin.xml :

[...]
<plugincomponents>
<datacontroller class="org.demo.jcmsplugin.demo.TitleController" types="Content" />
</plugincomponents>
[...]

WEB-INF/classes/org/demo/jcmsplugin/demo/TitleController.java :

[...]

public class TitleController extends BasicDataController {
private static Channel channel = Channel.getChannel();

public ControllerStatus checkIntegrity(Data data) {

if (!(data instanceof Content)) {
return ControllerStatus.OK ;
}
Content pub = (Content) data;

// Iterate on each site's language and check title is not empty
for (Iterator it = channel.getLanguageList().iterator(); it.hasNext();) {
String lang = (String) it.next();
String title = pub.getTitle(lang, false);

// If a title is missing return error status
if (Util.isEmpty(title)) {
ControllerStatus status = new ControllerStatus();
status.setProp("jcmsplugin.demo.title-missing");
return status;

}
}
return ControllerStatus.OK ;
}
}

3.3 Exécution du test unitaire

  • Dans Eclipse, faites un clic droit sur la classe de test, puis choisissez Run As > JUnit Test
unittest-runas

Fig. 2. Exécution du test unitaire

Le test unitaire passe avec succès :

unittest-success

Fig. 3. Succès du test unitaire

Ajoutez un bug dans le code du DataController (ou retirez le du plugin.xml par exemple), et relancez le test. Le test unitaire échoue.

unittest-failed

Fig. 4. Echec du test unitaire

Attention : Si l'exception suivante apparait dans la console :

UT FATAL [ChannelInitServlet.init:149] - An exception occured while initializing JCMS. The site is not available.
java.lang.NoClassDefFoundError: org/w3c/dom/ranges/DocumentRange
at java.lang.ClassLoader.defineClass0(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:539)

Il manque la librairie XML nécessaire au fonctionnement de JCMS en dehors d'un serveur d'application.
Vérifiez que la librairie xml-apis.jar est bien présente dans le classpath de votre projet.

4. Utilisations avancées

4.1 Utilisation de Log4j

Afin d'uniformiser les messages de log générés dans depuis les tests unitaires, un Appender log4j est automatiquement déclaré pour les Logger "unittest.*". Préfixez le nom de votre logger en utilisant "unittest." et vous bénéficierez de ces messages de logs.

Le Logger déclaré dans votre code ne change pas.
Exemple avec la classe MyClass qui doit être testée :

public class MyClass {
private static final Logger logger = Logger.getLogger(MyClass.class);
public void someMethod() {
logger.info("Hello World !");
}
}

Le Logger déclaré dans le code du test unitaire reprend et modifie la même déclaration.
Exemple avec la classe de test unitaire MyClassTest :

public class MyClassTest {
private static final Logger logger = Logger.getLogger("unittests." + MyClass.class.getName());
public void testSomeMethod() {
logger.info("Testing Hello World...");
...
}
}

Voici les logs qui ont lieu lors de l'exécution des test unitaires :

UT FATAL [MyClassTest.testSomeMethod:42] - Testing Hello World...
10:55:21,031 INFO [JCMS] [MyClassTest] - Hello World !

4.2 Mock objects

Certains développements ne peuvent être executés correctement qu'en présence d'un environnement J2EE non disponible lors des test unitaires (une session, des requetes HTTP, etc.)
Afin de tester ces développements, l'API de tests de JCMS fournie des classes qu'on appelle communément des Mock Objects permettant de reproduire partiellement ces environnements complexes.
Les méthodes suivantes de JcmsTestCase permettent de récupérer de nouvelles instances de ces mocks objects :

  • getMockHttpSession() : récupère une nouvelle session
  • getMockHttpServletRequest() : récupère une nouvelle requête HTTP et une nouvelle session
  • getMockHttpServletRequest(HttpSession) : récupère une nouvelle requête HTTP associée à la session passée en paramètre
  • getMockJcmsJspContext() : récupère une nouvelle instance de JcmsJspContext
  • getMockJcmsJspContext(HttpServletRequest) : récupère une nouvelle instance de JcmsJspContext associée à la requête passée en paramètre

Attention ! ces mocks objects ne sont pas entièrement fonctionnels :

  • certaines méthodes renvoient des valeurs arbitraires.
  • d'autres méthodes ne sont pas implémentées. En les appelant une exception NotImplementedException sera levée.

Malgré cela, ils fournissent une base de travail suffisante pour les tests unitaires. Si vous jugez utile qu'une méthode actuellement non disponible soit implémentée, n'hésitez pas à nous le signaler.

Voici un exemple pour tester un panier d'achats qui nécessite d'être stocké en session :

public class ShoppingCartTest {
public void testGetCart() throws Exception {
HttpSession session = getMockHttpSession();
ShoppingCart cart = ShoppingCart.getCart(session);
assertNotNull(cart);
}
}

Consultez la Javadoc pour plus d'informations sur ces classes.

4.3 Fichiers .release

Si des fichiers ".release" sont présents (dans le répertoire WEB-INF/data) pour le store.xml ou les propriétés (custom.prop ou webapp.prop), ils sont utilisés pour les tests unitaires.

  • store.xml.release est copié vers store.xml.junit
  • custom.prop.release est copié vers custom.prop.junit
  • webapp.prop.release est copié vers webapp.prop.junit

Ces copies (fichiers .junit) sont alors utilisées pour démarrer la webapp : Vous pouvez exploiter cette fonctionnalité pour effectuer vos tests en utilisant systématiquement un environnement prédeterminé.
Les fichiers  .junit sont des temporaires qui sont effacés à la fin des tests unitaires. Les fichiers .release sont laissés intact.

4.4 Propriétés

Certains tests unitaires ne peuvent être réalisés que si JCMS a été configuré d'une certaine manière (e.g. langues du site).
Pour éviter de devoir modifier les propriétés à chaque exécution des tests unitaires, plusieurs possibilités :

  1. Créer un fichier de propriété WEB-INF/data/junit.prop : il s'agit d'un fichier spécifique aux tests unitaires qui est chargé en dernier. Les propriétés qu'il contient remplacent les propriétés des autres fichiers .prop. C'est la méthode recommandé pour spécifier des priopriétés dédiées aux tests unitaires.
  2. Créer un fichier de propriété WEB-INF/data/custom.prop.release, c'est ce fichier qui sera utilisé comme base pour les test unitaires (cf section précédente 4.3).
  3. Modifier les propriétés dans le code avant d'effectuer le test en utilisant la méthode channel.updateAndSaveProperties(...) (et restaurer les propriétés après pour ne pas impacter les autres tests)
    Attention cependant, certaines propriétés de JCMS ne sont pas prises en compte sans un redémarrage, auquel cas seules les méthodes 1 et 2 sont utilisables. Vérifiez au cas par cas.

Exemple pour un test qui nécessite la configuration de JCMS en 3 langues, avec l'anglais en langue principale :

  String defaultLangBak = null;
String languagesBak = null;

private void setLanguages(String defaultLang, String languages) {
JProperties prop = new JProperties();
prop.setProperty("channel.default-lang", defaultLang);
prop.setProperty("channel.languages", languages);
channel.updateAndSaveProperties(prop);
}

protected void setUp() throws Exception {
// Backup current values
defaultLangBak = channel.getProperty("channel.default-lang");
languagesBak = channel.getProperty("channel.languages");

// Change to our test values
setLanguages("en", "en,fr");
}

protected void tearDown() throws Exception {
// Restore previous values
setLanguages(defaultLangBak, languagesBak);
}

public void testWhatever() {
...
}

4.5 Création de Publication et recherche textuelle

Dans JCMS, l'indexation des publications ayant lieu de manière asynchrone dans un thread dédié, la recherche textuelle d'une publication immédiatement après sa création pourrait ne pas renvoyer de résultat. Il est alors nécessaire de faire appel à une temporisation.

Exemple :

  public void testCreateAndSearch() throws Exception {
SmallNews sm = new SmallNews();
sm.setTitle("Time is on my side");
sm.setContent("whatever");
sm.setAuthor(admin);
sm.performCreate(admin);

try { Thread.sleep(1000); } catch(Exception ex) { }

QueryHandler qh = new QueryHandler();
qh.setTextSearch(true);
qh.setText("time side");
QueryResultSet qrs = qh.getResultSet();
assertTrue(qrs.contains(sm));
}

4.6 Transaction Hibernate pour JcmsDB

A partir de la version 6, JCMS utilise Hibernate pour gérer les données de JcmsDB en base de données relationnelle. Toutes les opérations de lectures et d'écritures nécessitent l'ouverture d'une transaction Hibernate.

Une transaction Hibernate est automatiquement ouverte pour chaque requête au serveur HTTP. Ainsi, la base de donnée peut être accédée par toute méthode participant au traitement de cette requête.

Lors de l'exécution des tests unitaires, vous êtes responsables de la gestion des transactions. Pour cela, vous devez invoquer les méthodes beginTransaction et commitTransaction dans vos tests unitaires, entre chaque manipulation de données JcmsDB.

Les objets JcmsDB manipulés dans une transaction Hibernate ne sont valides que pour la durée de cette transaction. Si vous devez référencer un objet sur plusieurs transaction, référencez son identifiant, et non l'objet lui même.

Voici un exemple pour la création d'un avis et son utilisation dans une seconde transaction :

String reviewId = null;
beginTransaction();

{
Review review = new Review();
review.setTitle("My review");
review.setAuthor(ws1Writer);
review.setWorkspace(ws1);
review.setReviewedPublication(pub);
review.setComment("Great job !");
review.performCreate(ws1Writer);
reviewId = review.getId();
}
commitTransaction();

beginTransaction();
{
Review review = channel.getData(Review.class, reviewId);
assertEquals("Great job !", review.getComment());
}
commitTransaction();

Attention, certaines fonctionnalités des données JStore utilisent également la base de donnée JcmsDB (e.g. le suivi des lecteurs, les notes de workflow, les ExtraDBData, etc). Si vous utilisez ces fonctionnalités, vous devez les encapsuler dans une transaction Hibernate.

Exemple pour la modification d'une ExtraDBData d'une publication JStore

beginTransaction();
{
publication.setExtraDBData("myplugin.foo", "bar");
}
commitTransaction();

 

References