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