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

Aucun commentaire:

Enregistrer un commentaire