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 :

Aucun commentaire:

Enregistrer un commentaire