jeudi 16 janvier 2014

Arquillian, JPA et Datasets (2/2) : Utiliser Arquillian Persistence Extension

Dans la première partie de l’article sur Arquillian, JPA et Dataset, je montrais comment paramétrer nos test pour gérer les transactions, l’injection de données. Au final, le résultat est assez compliqué à mettre en place. Dans cet article, je vais faire voir comment utiliser une Extension d'Arquillian pour nous faciliter la vie.

Tout d’abord, pour répondre aux problématiques évoquées dans l'article précédent,nous pourrions utiliser DBUnit ou Unitils, mais le problème est que ces frameworks ne sont pas compatibles avec CDI. Nous allons donc utiliser "Arquillian Persistence Extension". La solution est assez jeune (encore en Alpha !) mais elle est prometteuse car elle apporte des réponses et simplifie le travail du développeur. Je vais donc entrer plus en détail sur son utilisation dans cet article.

Arquillian Persistence Extension offre plusieurs fonctionnalités :
  • Assure la gestion des transactions pour chaque test unitaires 
  • Assure l’injection de jeux de données de tests spécifiquement pour chaque test dans différent formats. Cette fonctionnalité est offerte par @UsingDataSet 
  • Compare les données en fin de test avec un jeu de test attendu. offert par @ShouldMatchDataSet 
  • Eviction du cache de second niveau entre le passage des tests. 
Je ne reviens pas ici sur les Entity et Dao expliqué au chapitre précédent, ce sont les mêmes. Je vais me concentrer sur la classe de test.

 

 Un peu de configuration

Malgré que H2 ne soit pas indiqué explicitement comme étant supportée, j’ai fais mes tests sur cette version et confirme qu’elle est fonctionnelle avec l’extension persistence.
Il faudra juste prendre en compte deux particularités : Le Dialecte doit être pris en compte dans le persistence.xml
<properties>
    <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
    <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    <property name="hibernate.show_sql" value="true"/>
    </properties>

Le DataTypeFactory doit être prise en compte dans le fichier de configuration arquillian.xml pour pouvoir s’interfacer avec H2.
<extension qualifier="persistence-dbunit">
    <property name="datatypeFactory">org.dbunit.ext.h2.H2DataTypeFactory</property>
</extension>
Enfin, on ajoute la dépendance Maven
<dependency>
  <groupId>org.jboss.arquillian.extension</groupId>
  <artifactId>arquillian-persistence-impl</artifactId>
  <version>1.0.0.Alpha6</version>
  <scope>test</scope>
</dependency>
Une fois la dépendance Maven apportée, nous avons accès à plusieurs annotations (dont @UsingDataSet et @ShouldMatchDataSet) qui vont nous simplifier la vie.

 

La classe de test

Dans notre classe de test WeatherDaoImpl, je ne définis pas d'EntityManager, ni de UserTransaction. La prise en compte de la transaction est automatique dès lors que la méthode est annotée avec @UsingDataSet ou @ShouldMatchDataSet. Pour le développeur, pas besoin de créer des transactions manuellement, ni même d’insérer l’EntityManager à ce niveau.
@RunWith(Arquillian.class)
public class WeatherDaoTest {
  
    @EJB
    WeatherInfoDao dao;

    @Test
 ...

 

@UsingDataSet

L’annotation @UsingDataSet permet d’injecter un jeux de données. Le fichier peut être au format XML, JSON, YAML ou même SQL. Le format est automatiquement pris en compte grâce à l’extension du fichier.
@Test
@UsingDataSet("datasets/weather.json")
public void accessTemperature() {
  
    WeatherInfo info = dao.getInfoFromTown("NTE");
    Assert.assertEquals("32°C", info.getTemperature());
}

Exemple de fichier de DataSet
{
"WeatherInfo":
  [
    {
      "townCode" : "NTE",
      "townName" : "Nantes",
      "temperature" : "32°C",
      "isRainingInAnHour" : false
    },
    {
      "townCode" : "BRT",
      "townName" : "Brest",
      "temperature" : "32°C",
      "isRainingInAnHour" : false
    }
  ]
}

Le même DataSet en YAML offre plus de concision.Je pars sur ce format dans l'article, mais il faut savoir que le développeur est libre de choisir le format selon ses besoins au cas par cas (un test en JSON, un autre en XML par exemple). Cependant, je préconiserais quand même d'avoir une uniformisation des formats de fichiers pour simplifier la maintenabilité de l'application.
WeatherInfo:
  - townCode: NTE
    townName: Nantes
    temperature: 32°C
    isRainingInAnHour: false
  - townCode: BRT
    townName: Brest
    temperature: 18°C
    isRainingInAnHour: true

Quelques points à savoir sur l’utilisation des DataSets :
  • la valeur de la PK des enregistrements n’est pas obligatoire dans le DataSet. Elle sera auto-incrémentée automatiquement lors de chaque ajout. mais... 
  • Persistence extension s’appuie sur DBUnit. Lors de l”ajout, la PK est incrémentée mais la valeur de la séquences utilisée par l’AUTO_INCREMENT n’est pas mise à jour. Ceci est un important à savoir car si l’on souahite ajouter un nouvel enregistrement depuis le test, celui-ci risque de se positionner sur un enregistrement existant (le numéro 1 par exemple) et lancer une exception d’unicité (du type : A different object with the same identifier value was already associated with the session). 
  • Lorsque l’on fait des tests de création, il est donc indispensable d’ajouter explicitement les valeurs de PK dans les datasets en les positionnant à des valeurs hautes pour éviter les collisions (c’est à dire supérieur au nombre d’enregistrement qui pourraient être créés pendant les tests). 
  • Lors des tests de lecture ou de modification de données existantes, les valeurs de PK pourront être omises. 
Ainsi, dans ce second test :
@Test
@UsingDataSet("datasets/weather2.yml")
public void createNewInfo() throws Exception {
    WeatherInfo info = new WeatherInfo("LMS", "Le Mans", "28°C", false);
     
    dao.saveInfo(info);
    Assert.assertEquals(3, dao.getAllInfos().size());

}

on effectue l'injection avec le DataSet weather2.yml suivant :
WeatherInfo:
  - id: 998
    townCode: NTE
    townName: Nantes
    temperature: 32°C
    isRainingInAnHour: false
  - id: 999
    townCode: BRT
    townName: Brest
    temperature: 18°C
    isRainingInAnHour: true

 

@ShouldMatchDataSet

L’annotation @ShouldMatchDataSet ajoute une assertion supplémentaire sur l’état de la base de données attendue en fin d’un test. Il indique donc un DataSet résultat attendu. Si l’état de la base est identique au Matching Dataset, alors le test est OK. Si la base est dans un autre état, le test est en échec.
Pour les matching Datasets, il est possible (et d’ailleurs très préférable) d'exclure certaines colonnes. Par exemple, la colonne des valeurs de PK n’as en général aucun intérêt à être testée et peux donc être exclus avec excludeColumns.

@Test
@UsingDataSet("datasets/weather2.yml")
@ShouldMatchDataSet(value="datasets/expected-weather2.yml", excludeColumns="id")
public void createNewInfo() throws Exception {
  
    WeatherInfo info = new WeatherInfo("LMS", "Le Mans", "28°C", false);
    dao.saveInfo(info);
}
WeatherInfo:
  - townCode: NTE
    townName: Nantes
    temperature: 32°C
    isRainingInAnHour: false
  - townCode: BRT
    townName: Brest
    temperature: 18°C
    isRainingInAnHour: true
  - townCode: LMS
    townName: Le Mans
    temperature: 28°C
    isRainingInAnHour: false

A savoir :
  • On peut également utiliser @ShouldMatchDataSet seul, sans avoir au préalable inséré de Dataset avec @UsingDataSet. Ca peut être utile lors des tests de méthode de création. 
  • La méthode de test annotée @ShouldMatchDataSet est executée au sein d’une transaction.
@Test
@ShouldMatchDataSet(value="datasets/expected-weather2.yml", excludeColumns="id")
public void createNewInfoFromScratch() throws Exception {
     
    WeatherInfo info1 = new WeatherInfo("NTE", "Nantes", "32°C", false);
    WeatherInfo info2 = new WeatherInfo("BRT", "Brest", "18°C", true);
    WeatherInfo info3 = new WeatherInfo("LMS", "Le Mans", "28°C", false);
        
    dao.saveInfo(info1);
    dao.saveInfo(info2);
    dao.saveInfo(info3);
}

 

@Transactional

Cette annotation permet de rendre une méthode de test transactionnelle, simplement en annotant la méthode ! Cette annotation est prise en compte par le Arquillian Transaction Extension. (extension dont je n’ai pas parlé auparavant mais qui est tiré de manière transitive par le Arquillian Persistence Extension). Pour rappel, les tests classiques (simplement annoté avec @Test) ne sont pas exécutes dans un contexte transactionnel.
Par exemple, le code ci-dessous finit en erreur car il s’exécute hors transaction. (Pour rappel, le DAO est marqué pour s'exécuter au sein d'une transaction car TransactionAttribute est MANDATORY).

@Test
public void createNewInfoFromScratchWithoutTransaction() throws Exception {
  
    WeatherInfo info1 = new WeatherInfo("NTE", "Nantes", "32°C", false);
    WeatherInfo info2 = new WeatherInfo("BRT", "Brest", "18°C", true);
         
    dao.saveInfo(info1);
    dao.saveInfo(info2);
   
    Assert.assertEquals(2,  dao.getAllInfos().size());
}

Dans ce cas, le TU doit donc obligatoirement être transactionnel. Pour résoudre cette erreur, il suffit simplement d’annoter la méthode @Transactional.

import org.jboss.arquillian.transaction.api.annotation.Transactional;
... 
@Test
@Transactional
public void createNewInfoFromScratchWithTransaction() throws Exception {
    WeatherInfo info1 = new WeatherInfo("NTE", "Nantes", "32°C", false);
    WeatherInfo info2 = new WeatherInfo("BRT", "Brest", "18°C", true);
        
    dao.saveInfo(info1);
    dao.saveInfo(info2);
     
    Assert.assertEquals(2,  dao.getAllInfos().size());
}

 

En conclusion

L’extension nous permet de simplifier énormément l’écriture de tests JPA dans un contexte JavaEE par rapport à l’article précédent. Le code reste clair, facile à comprendre et la dissociation entre code et données de tests est un gros avantages pour la maintenabilité des tests.

De plus, L’extension offre des possibilités complémentaires : injection de scripts SQL, création de schéma (si utilisation hors ORM par exemple), insertion SQL en @Before/@After, définition de stratégies de Cleanup, éviction de cache de second niveau.

En revanche, ce qui pêche un peu, c’est le manque de documentation. J’ai du dépouiller le code source pour comprendre comment utiliser l’appli. Cependant, même en Alpha mais est déjà fonctionnelle et pourra vous simplifier la vie lors de la création des Tests de vos application JavaEE.

A tester donc !

Pour aller plus loin

Les sources des l'article sont disponibles sur Github.
 Les sources du projet “Arquillian Persistence Extension” sont disponibles sur Github.
Comprendre les problématiques d’unicité de valeurs de PK dans DBUnit : http://sipxconfig.blogspot.fr/2005/03/dbunit-seed-data-use-high-primary-ids.html

mercredi 8 janvier 2014

Arquillian, JPA et Datasets (1/2) : Première prise en main

Le premier article Tester son application JavaEE avec Arquillian montrait comment effectuer des tests sur des composants EJB.

Lors de l’écriture des tests unitaires, il est nécessaire de tester les composants touchant la couche persistance JPA dans un contexte transactionnel. Nous allons donc voir comment prendre en compte la persistance et les transactions lors des tests avec Arquillian.

Quelques points à savoir avant de commencer :
  • Lors du passage des tests unitaires, il est important d’isoler les données utilisés pour les tests de ceux utilisées pour l’intégration. Il est donc nécessaire de créer une base dédiée pour les tests. Créer une base "manuellement" pour chaque test est trop long et pas industriel. Il est donc nécessaire de prendre une base embarquée type HSQLDB ou H2. Ici, nous prenons une base de donnée embarquée H2 car elle est déjà intégrée dans le profile par défaut JBoss, il n’y a donc pas de configuration supplémentaire à faire dans les drivers JDBC. 
  • il est préférable de garder les TU au sein d’une transaction. L’état de la base reste propre entre chaque passage de test et les données modifiées pendant les tests doivent être rollbackées.
  • C’est lors des tests de persistance qu’Arquillian prend tout son sens par rapport à Spring Test. En effet, lors de l’écriture de l’article précédent, comme on ne travaillait qu’avec des EJB, on aurait pu utiliser Spring Test, puisqu’il interprete certaines annotations standards (notamment @Inject, @EJB). A partir du moment ou l’on utilise JPA, Datasources, Transactions, Producers et Resources alors Spring Test n’est plus en capacité de répondre à nos besoins : il nous faut un conteneur EE.
  • En corollaire du point précédent, les tests en mode "JBoss Embedded" ne sont plus possible avec JPA et les transactions. Il faudra utiliser un déploiement en Container : managed ou remote.
  • Le site d’Arquillian propose un article complet disponible à cette adresse : http://arquillian.org/guides/testing_java_persistence/ Cet article est une bonne base et nous allons voir comment aller plus loin en injectant des données et gérer les transactions.

 

Création de l’Entité JPA

L’entité JPA assure le mapping avec la base de données. Dans le cas de test, l’Entity WeatherInfo porte des infos de temps et est mappée avec une table. Les détails de l’implémentation de la table ne nous importe pas ici, on va laisser l'ORM générer la table.
L’entité doit être annotée @Entity et contenir une clé primaire représentée par @Id.
@Entity
public class WeatherInfo implements Serializable {
     
    @Id @GeneratedValue
    private long id;
    private String townName;
    private String townCode;
    private String temperature;
    private boolean isRainingInAnHour;
    ...

 

Création du Dao

Le Dao (Object d’Accès au Donnés) sur l’entité WeatherInfo est représenté sous la forme d’un EJB Stateless avec l’annotation @Stateless.
L’EntityManager est injecté dans l’EJB avec @PersistenceContext. Pour simplifier l’exemple, les méthodes du Dao effectuent des créations de requêtes depuis le PersistenceContext. J’ai annoté le Dao en tant que @TransactionAttribute = MANDATORY : Je force le Dao a être marqué pour s'exécuter au sein d'une transaction. Ce flag est une sécurité et assure qu'il est bien appelé au sein d’une transaction déjà ouverte. Pour le respect de l’architecture en couche, c’est primordial. Si le Dao est appelé depuis un service, alors la transaction est portée par le service et est utilisée par le Dao. Si un autre appelant (service IHM par exemple) appelle directement le Dao hors transaction, c’est qu’il ne respecte pas les couches de l’architecture et n’est pas "autorisé" à être appelé (ce qui finira en Exception).

@Stateless
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public class WeatherInfoDao {

    @PersistenceContext
    EntityManager em;

    public WeatherInfo getInfoFromTown(String townId) {
        WeatherInfo info = em.createQuery("select w from WeatherInfo w where w.townCode='"
                          + townId + "'", WeatherInfo.class).getSingleResult();
        return info;
 }

 

Configuration JPA

Le fichier persistence.xml est disponible dans src/test/resources/test-resource.xml. Il est injecté dans l’archive grace à Shrinkwrap et renommé en persistence.xml pour que le mapping JPA soit pris en compte. (je ne m’étends pas sur les spécificités JPA ici). Ce fichier définit le jta-data-source à utiliser ainsi que les propriétés spécifiques de l'ORM, et pour lequel on demande la création du schéma (create-drop). le dialect doit être org.hibernate.dialect.H2Dialect pour H2.

<persistence version="2.0" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns="http://java.sun.com/xml/ns/persistence" xsi:schemalocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="test">
        <jta-data-source>jdbc/arquillian</jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

 

Déploiement du datasource

Même si je fais les tests dans une base de données embarquées, je dois définir un Datasource H2 et le déployer au sein de JBoss.
Pour les tests, je peux très bien intégrer ce datasource dans le WEB-INF du WAR pour qu’il soit déployé en même temps que l’application. (les fichiers nommés en *-ds.xml sont reconnus et déployés en tant que datasource, c’est pour cette raison que le fichier weather-ds.xml en pris en compte en tant que datasource lors du déploiement de l’application).
Cette façon de faire est donc idéale pour les micro-déploiements des tests unitaires.
Ce datasource est défini comme suit :
<datasources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns="http://www.jboss.org/ironjacamar/schema" 
        xsi:schemalocation="http://www.jboss.org/ironjacamar/schema
        http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
    <datasource enabled="true" jndi-name="jdbc/arquillian" pool-name="ArquillianEmbeddedH2Pool">
        <connection-url>jdbc:h2:mem:arquillian;DB_CLOSE_DELAY=-1</connection-url>
        <driver>h2</driver>
    </datasource>
</datasources>
Ce qui est important à voir ici, c’est le JNDI name qui est le même que celui référencé dans le persistence.xml.

 

Ecriture de notre classe de tests

Je reprends le même cas de test de l’article précédent, que je vais étoffer pour prendre en compte les tests de Dao. Dans la classe de test WeatherDaoTest, j'injecte notre Dao, puis l’EntityManager ainsi que le UserTransaction.
@RunWith(Arquillian.class)
public class WeatherDaoTest {
    @EJB
    WeatherInfoDao dao;

    @PersistenceContext
    EntityManager em;
     
    @Inject
    UserTransaction utx;

On remarque qu’il faut explicitement enlister l’EntityManager dans la transaction JTA. Cette étape est nécessaire car j'utilise les deux ressources indépendamment. Cela peut sembler anormal si on utilise JPA depuis un EJB car dans ce cas, l’enlistement est automatique, mais ici, il faut l’ajouter explicitement. Arquillian exécute les méthodes @Before et @After dans le conteneur, respectivement avant et après les méthode de tests.
 La méthode @Before est invoquée après que les injections (EJB, em et utx) aient eu lieu.
@Before
    public void preparePersistenceTest() throws Exception {
        clearData();
 insertData();
 startTransaction();
    }
J'ai besoin d’assurer une injection et suppression de données et pour chaque cas de test. Ces injections/suppressions de données sont prises en compte dans les méthodes clearData() et insertData() et appelées depuis la preparePersistenceTest().
preparePersistenceTest() est appelé dans le @Before, la transaction n’est donc pas encore ouverte. Il faut que ces méthodes aient la responsabilité d’ouverture/fermeture des transactions pour assurer l’ajout de données. L’ajout de données est réalisé en créant les entités et en les persistant :

private void insertData() throws Exception {
    utx.begin();
    em.joinTransaction();
     
    // on ajoute des objets
    WeatherInfo info1 = new WeatherInfo("NTE", "Nantes", "32°C", false);
    WeatherInfo info2 = new WeatherInfo("BRT", "Brest", "18°C", true);
     
    em.persist(info1);
    em.persist(info2);
     
    utx.commit();
    // clear the persistence context (first-level cache)
    em.clear();
}

private void clearData() throws Exception {
    utx.begin();
    em.joinTransaction();
    em.createQuery("delete from WeatherInfo").executeUpdate();
    utx.commit();
}
Une fois les ajouts de donnés effectués, la méthode preparePersistenceTest() va lancer la transaction en appelant le startTransaction() qui va ouvrir la transaction pour les tests.

private void startTransaction() throws Exception {
    utx.begin();
    em.joinTransaction();
}

Une fois le test effectué, la méthode annotée @After est appelée et dans notre cas commiter la transaction.

@After
public void commitTransaction() throws Exception {
    utx.commit();
}

Les sources complètes de l’application de tests sont disponibles ici. Je vous invite à y jeter un œil pour mieux comprendre ce qu’elles font. Les tests sont ici présentés en utilisant Wildfly !!!! Attention, pour Wildfly, il faut modifier la dépendance, et pour l’instant, Wildfly n’est pas en Release et donc le container plugin n’est pas pas encore disponible en version Release non plus. La seule différence pour nous est de prendre en compte le container dans la version adéquate, c’est à dire :

<dependency>
  <groupId>org.wildfly</groupId>
  <artifactId>wildfly-arquillian-container-managed</artifactId>
  <version>8.0.0.Beta1</version>
  <scope>test</scope>
</dependency>

 

En conclusion

C'est une approche intéressante et fonctionnelle, mais qui reste compliquée :
  • Beaucoup de code technique et qui nécessite une maitrise du cycle de vie des test unitaires et des transactions (@Before, @After...).
  • Des méthodes de chargement de données communes à toutes les méthodes de tests. Peut-être que dans certains cas de tests précis, on souhaiterait ne charger que certains lots de données : avec cette approche, on ne peut pas.
  • Une création de données depuis du code Java, explicite et donc très (trop!) verbeux lorsque l’on dispose de beaucoup de jeux de données. De plus, le code Java n'est pas non plus le format le plus adapté pour représenter des jeux de données (difficulté de lecture).
  • Pas de tests sur le jeu de données attendu. Comment assurer que les données de sorties sont bien celles que nous attendions pour notre cas de test précis ?
Bien sûr, toutes ces problématiques pourraient être résolues, mais demandent beaucoup de code technique pour être mis en place. Dans un prochain article, nous allons voir comment tirer parti de l’extension Arquillian Persistence Extension pour faciliter les tests Arquillian avec JPA et datasets.

Pour aller plus loin :