Affichage des articles dont le libellé est bonnes pratiques. Afficher tous les articles
Affichage des articles dont le libellé est bonnes pratiques. Afficher tous les articles

jeudi 27 novembre 2014

La Loi de Dietzler

La Loi de Dietzler, énoncée par Neal Ford lors de sa conférence “Abstraction Distraction” ( et qui n’est pas sans rappeler le Principe de Pareto), évoque une loi empirique selon laquelle un outil informatique présentant une abstraction élevée ne pourra jamais répondre à 100% des besoins de l’utilisateur.

Cette loi a été initialement rapportée par Terry Dietzler, un collègue de Neal Ford, qui travaillait alors sur des projets Access. Neal Ford l’étend à tous les Langages de 4ème génération.

 Il définit trois catégories :

  •  80% des besoins seront rapides et facile à créer. 
  • 10% des besoins seront possibles à créer mais nécessiteront d’adapter l’outil, le contourner, le “tordre”. 
  • 10% des besoins restants seront impossibles à créer car l’utilisateur sera emprisonné par une abstraction trop élevée. 
 Si un outil est prévu pour un cadre trop idéal et présente un niveau d’abstraction trop élevé sans offrir de possibilités de sortir de ce cadre, alors l’utilisateur va se sentir frustré de ne pas pouvoir en tirer parti intégralement et va, à terme le délaisser.

 Selon Ford, les L4G suivent ce cadre : ils permettent d’implémenter très rapidement des problématiques générales, mais présentent trop d’abstractions ce qui rend compliqué - voire impossible - l’implémentation des cas particuliers. En pratique, on se rend compte qu’aucun cadre n’est idéal et que beaucoup de développeurs - si ce n'est tous - ont besoin d’implémenter des cas particuliers.

 Ce que l’on peut tirer de cette loi, c’est que si l’on souhaite mettre en place un outil offrant un niveau d’abstraction élevé, il faut toujours laisser à l’utilisateur la possibilité d’accéder à la couche de plus bas niveau (sous cette abstraction), pour qu’il puisse prendre en compte les exceptions représentées par les cas particuliers à son contexte.

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 :

vendredi 29 novembre 2013

Tester son application JavaEE avec Arquillian

La bonne pratique de faire des tests unitaires dans son application est rentrée dans les moeurs du développeurs Java, JUnit étant le plus connu et utilisé. Il permet de répondre facilement aux cas de tests standards. Mais les problématiques arrivent très vite dès lors que l’on souhaite tester en environnement EE : comment effectuer des tests unitaires sur les EJB, sur les services d’accès aux données, comment prendre en compte l’aspect transactionnel et les données sans polluer une base existante ?

Autant de questions qui peuvent trouver une réponse grâce à Arquillian, un framework de test pour JavaEE.


Arquillian permet de tester son code dans un environnement proche des conditions réelles de l’application, c’est à dire au sein d’un conteneur JavaEE. A ce titre, Arquillian propose différents type de conteneurs pour ces tests.

Quelle différence entre le mode de conteneur “Embedded”, “Managed” et “Remote” ?
On peut dissocier le mode embarqué (Embedded) du mode “In-container” (Managed ou Remote).
Le mode embarqué (Embedded) est une “émulation de conteneur” : le process du mode embarqué est exécuté dans le même process que le test et agit comme un conteneur mais n’en est pas un. Il y’a donc des risques de différences de comportement entre ce mode et le conteneur réel. De plus, le mode embarqué se réduit à l’injection CDI : certaines fonctionnalité ne sont pas prises en compte (JPA) réduisant son intérêt dans certains cas.

Le mode in-container (Managed ou Remote) tire parti d’un conteneur réel qui peut être distant ou managé.
Le mode distant (Remote) utilise un conteneur hors de l’environnement de test. Ce mode présume que le conteneur soit déjà démarré : lors de l’exécution du test unitaire, Arquillian n’a pas la main sur le démarrage/arrêt du conteneur. C’est donc au développeur de s’assurer que le conteneur soit démarré avant de lancer ses tests. Le mode embarqué est conseillé en phase de développement pendant lequel le développeur peut placer le conteneur en Debug et ainsi faciliter l’analyse et la correction de bugs.

Le mode managé (managed) est simplement un conteneur distant qui prend également en compte son cycle de démarrage et d’arrêt. Contrairement au conteneur embarqué, le processus du conteneur managé est exécuté dans un process JVM séparé. Une fois que le serveur est démarré, il se comporte comme le conteneur distant, c’est à dire qu’Arquillian intéragit avec lui à travers d’un protocole distant (comme HTTP).
Le conteneur managé est plus utile dans le cadre d’un environnement d’intégration continue dans lequel l’on souhaite tester sur un conteneur complet, mais sans savoir s’il est déjà démarré. Arquillian s’occupe donc de démarrer le conteneur avant d’effectuer les tests, puis de l’éteindre une fois qu’ils sont terminés.

micro-déploiement par classe de tests

L’utilisation d’Arquillian sur les cas classiques est très bien expliqué sur le tutoriel du site, je ne vais donc pas m’attarder sur le sujet et redirige donc vers le site officiel : http://arquillian.org/guides/getting_started/ beaucoup mieux expliqué que je ne le ferais, je vous conseille donc d'y jeter un oeil.

En résumé, pour réaliser un test Arquillian, il faut créer une classe de tests annotée @RunWith(Arquillian.class). Cette annotation précise à JUnit d’utiliser Arquillian comme controleur de test. Arquillian recherche ensuite toutes les méthodes annotées avec @Deployment pour récupérer l’archive de test que l’on appelle le micro-deploiement. Le but de ce micro-déploiement est d’isoler les classes et les ressources qui sont utilisées pour le tests.

Contrairement à un test normal JUnit, Arquillian ne s’appuie pas sur l’intégralité du Classpath. Il faut indiquer seulement les ressources dont nous avons besoin pour les tests. L’archive est crée avec Srinkwrap, l’API de création d’archive (JAR, EAR, WAR). Cette approche permet de concentrer les tests exclusivement sur les classes utiles et permet de garder des tests propres et maintenables.
@Deployment
public static WebArchive createDeployment() {
  
    WebArchive webArchive = ShrinkWrap.create(WebArchive.class)
                .addAsWebInfResource("META-INF/ejb-jar.xml")
                .addAsWebInfResource("META-INF/beans.xml")
                .addPackages(true, "fr.lynchmaniac.arquilliantest");
    return webArchive;
}
Le composant à tester est injecté en annotant l’attribut @EJB ou @Inject.
@EJB
WeatherWs ws;
Le positionnement des annotations @Test définir les méthodes de tests constitue les méthodes de tests unitaires. Sur ce dernier point, les tests Arquillian ressemblent comme deux gouttes d'eau à un test JUnit.
@Test
public void rainingTest() {
    Assert.assertFalse(ws.isRainingInAnHour("Nantes"));
}
Comme expliqué précédemment, il est préférable d’utiliser le mode Remote ou Managed. Dans notre cas, nous utilisons JBoss 7.1.1, les dépendances tirées sont donc, pour le mode Managed :
<dependency>
    <groupid>org.jboss.as</groupid>
    <artifactid>jboss-as-arquillian-container-managed</artifactid>
    <version>7.1.1.Final</version>
    <scope>test</scope>
</dependency>
Et pour le mode remote, nous utilisons :
<dependency>
    <groupid>org.jboss.as</groupid>
    <artifactid>jboss-as-arquillian-container-remote</artifactid>
    <version>7.1.1.Final</version>
    <scope>test</scope>
</dependency>
A noter que les artefacts suivants ne sont pas disponibles par défaut sur le dépôt Central Maven. Il faudra ajouter les dépôts suivants :
<repository>
    <id>jboss.org</id>
    <url>https://repository.jboss.org/nexus/content/repositories</url>
</repository>
    
<repository>
     <id>JBoss 3rd Party Releases</id>
  <url>https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/</url>
</repository>

Quelques conseils : 
  • Attention à n’ajouter que ce dont vous avez besoin au micro-déploiement. Si vous avez besoin d'une classe, n'ajoutez pas le package complet. En effet, ajouter trop de classe dans une archive augmente de temps de création et de déploiement. 
  • Les classes de tests (i.e. celles qui lancent les tests), n’ont pas besoin d’être présente dans l’archive, veillez à les exclure du micro-déploiement. 
  • Attention à bien définir la variable d’environnement JBOSS_HOME qui est nécessaire, en mode Managed pour lancer le serveur. 
  • Réduire au maximum le chargement des modules JBoss afin d’en réduire autant que possible le temps de chargement. Il peut être utile de créer son propre standalone.xml utilisé exclusivement pour le passage des TU. Dans notre cas de tests, nous avons réduit au chargement des modules suivants dans JBoss: {org.jboss.as.} configadmin,connector, ee, ejb3, jmx, jpa, logging, naming, osgi, remoting, security, threads, transactions, web, webservices,weld 
  • Définir un profil Maven de développement, actif par défaut, qui inclus l’artefact pour le déploiement Arquillian en mode Remote. Un profil "ci" peut être créé pour inclure l’artefact en mode "managed".
  • Dans le cas pour lequel vous souhaiter intégrer des artefacts projet à votre micro-déploiement via le Dependency Resolver, veillez à garder en tête les phases du cycle de vie Maven : test -> package -> install. Si vous souhaitez tirer parti d’un artefact pendant les tests, il faudra prévoir un passage de tests en 2 étapes : installer d’abord l’artefact en skippant les tests (mvn install -DskipTests) pour qu’il soit présent dans le dépôt, puis ensuite effectuer le test (mvn test).

Micro-déploiement par ensemble de classe de tests

Cette approche assure un premier niveau de test basique et fonctionnel. Il s’avère que sur un projet plus conséquents, comportant un grand nombre de classes de tests, cette approche s’avère très longue lors du passage des TU. En effet, chaque classe de test définit son propre micro-déploiement, mais nécessite également des phases de déploiement et de retrait des archives (en l'occurrence très souvent des WAR). Il s’agit clairement d’une limitation dans notre cas, car la création et le déploiement d’une archive est très coûteuse en temps. De ce fait, un grosse partie du temps de passage des TU est passé dans les tâches qui pourraient être mutualisées : création, déploiement et retrait de l’archive.


 En effet, pour des raisons d’optimisation, il est plus judicieux, d’augmenter le nombre d'éléments dans le micro-déploiement (qui sera un peu plus gros), mais d’assurer cette création et ce déploiement une seule fois pour tous les tests. Nous laissons tomber le déploiement par classe de test mais assurons un déploiement par unité " fonctionnelle ". L’objectif est donc de mutualiser la création et le déploiement afin d’éviter qu’il y ait un surcoût pour chaque nouveau test.

 Nous utilisons une extension d'Arquillian afin de détourner la cinématique de test à des fins d'amélioration de temps de passage des tests. La cinématique de test est donc la suivante :


 Arquillian ne propose pas cette fonctionnalité par défaut à l’heure actuelle (prévue dans la Roadmap de la version 2.0), mais nous allons voir comment tirer partie des extensions pour effectuer un micro-déploiement mutualisé pour une Suite de test.

Définition de la classe d’extension.
La classe d’extension ArquillianSuiteExtension est une classe technique qui implémente org.jboss.arquillian.core.spi.LoadableExtension. Je n’entre pas en détail dans l’implémentation de cette classe, les sources sont disponibles sur Github. Elle intercepte les évènements du cycle de vie d’Arquillian (démarrage, deploy, etc.). L’idée derrière cette classe est d’intercepter le démarrage d’un test Arquillian et de forcer à constituer le déploiement à partir d’une classe spécifique, au lieu de le lire dans chaque classe de test.
La classe contenant le déploiement est indiqué dans le fichier de configuration arquillian.xml :
<extension qualifier="suite">
    <property name="deploymentClass">fr.lynchmaniac.test.Deployments</property>
</extension>

Dans notre exemple, le nom qualifié ici fr.lynchmaniac.test.Deployments désigne la classe chargée de mutualiser la création de l’archive de test pour l’ensemble du projet.

Enregistrement de la classe d’extention
Pour être prise en compte, la classe d’extension doit enregistrée. C’est SPI qui est utilisé pour prendre en compte cette extension. le fichier META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension doit être présent dans le CLASSPATH et contenir le chemin vers la classe d’extension :
fr.lynchmaniac.test.ArquillianSuiteExtension

Classe de définition du micro-déploiement
La classe fr.lynchmaniac.test.Deployments doit contenir une méthode annoté @Deployment qui constituera le micro-déploiement complet pour un domaine. 

Attention : contrairement au premier chapitre, dans le cadre d’un déploiement mutualisé, les classes de tests doivent être présentes dans l’archive du micro-déploiement.

 @Deployment
    public static WebArchive deploy() {
    
     WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "ARQUILLIAN-TEST.war");
        
        webArchive.addPackages(true, "fr.lynchmaniac.arquilliantest")
                // dans ce cas, la classe de test doit être inclus dnas le WAR !
                .addClass(WeatherWsTest.class)
  .addClass(WeatherWs2Test.class)
                .addAsWebInfResource("META-INF/ejb-jar.xml")
                .addAsWebInfResource("META-INF/beans.xml");
 
     return webArchive;

    }

Les classes de tests
Cette classe ne doit surtout pas contenir d'annotation @Deployments, mais doit posséder au moins une annotation @Test.
@RunWith(Arquillian.class)
public class WeatherWsTest {

    @EJB 
    WeatherInfoService wis;
 
    @Test
    public void rainingTest() {
        Assert.assertFalse(wis.isRainingInAnHour("Nantes"));
    }
}

En conclusion

Ce premier article sur Arquillian, réalisé conjointement avec Vincent PIARD, a été rédigé dans l’objectif de donner un retour d’expérience sur les problématiques rencontrées pour les tests unitaires sur des architectures JavaEE. Nous avons utilisé Arquillian depuis plus d’un an sur un projet complexe et ce framework nous a aidé à les résoudre en partie.

En dernier conseil, il est important de garder à l’esprit plusieurs éléments essentiels pour faciliter les développements de tests unitaires dans ce cas :

  • Assurer un couplage lâche des composants est essentiels pour le passage des tests. Ceci permet de faciliter la création de mock et d’assurer une meilleure isolation des tests. 
  • Ne pas sur-découper les projets dans l’IDE et les artefacts (JAR en l’occurence). Un découpage fonctionnel ne veut pas obligatoirement dire un découpage technique" (i.e. un projet IDE/maven différent). Avoir trop de découpage en sous module technique complexifie les dépendances et le développement dans l'IDE. Cette complexité rend la constitution des micro-déploiements difficile à gérer.
  • Il est illusoire de penser que tous les TU doivent avoir une couverture de test de 100%. Vouloir atteindre 100% de couverture de test est illusoire et coûtera bien trop cher par rapport à la valeur ajoutée ! Préférez une approche réaliste : rester dans une fourchette de 2 à 5 tests. L’idéal est à minima de tester avec des entrées vides et au minimum et de tester avec des entrées complètes (cas à vide et cas optimal).
Dans le prochain article, nous verrons comment tirer parti d’Arquillian pour effectuer des tests de persistances.

Pour aller plus loin


Pour voir le détail de l'implémentation notamment toute la mécanique technique portée par ces tests, je vous encourage à jeter un oeil aux sources disponibles sur Github.

Autres sources intéressantes :

vendredi 12 avril 2013

Les 10 commandements d'un développeur humble

Les 10 commandements d'un développeur humble,  (en anglais, the 10 commandments of egoless programming) sont apparus dans le livre de Gerald Weinberg : "The Psychology of Computer programming" dont la publication originale date de 1971, mais restent toujours d'actualité !

I. Comprend et accepte que tu feras des erreurs.

II. Tu n'es pas ton code

III. Peu importe combien de Karaté tu connais, quelqu'un d'autre en connaitra toujours plus

IV. Ne ré-écrit jamais le code d'un autre sans consultation

V. Traite les gens qui en connaissent moins que toi avec respect, égard et patience

VI. La seule constante dans le monde est le changement

VII. La seule autorité provient du savoir, pas de la position

VIII. Bat toi pour ce que tu crois, mais accepte la défaite avec dignité.

IX. Ne soit pas "le mec dans son coin"

X. Critique le code et pas les personnes. Soit respectueux envers les personnes, pas envers le code.


A méditer au quotidien....

lundi 28 janvier 2013

5 Bonnes raisons du MDA pour le développement mobile

 
Article co-écrit par Florent Dupont et Lamia Gaouar, qui prépare actuellement un doctorat SIC autour des développements Android et MDA à l'université de Tlemcen.

La démocratisation des plateformes nomades (mobiles et tablettes) poussent les entreprises à proposer toujours plus de services aux utilisateurs. Elles ont, en effet, bien compris tous les enjeux qui se trament derrière les applications mobile : communication améliorée, nouveaux services aux utilisateurs, régie publicitaire, avantage concurrentiel, etc.

Cet engouement pour les plateformes nomades offre de nouvelles perspectives de marché, notamment pour les entreprises éditrices (que ce soient des Mobile Agencies, des Web Agencies ou encore des SSII) qui peuvent proposer leur expertise de développement. Depuis 2011, le marché des ventes de mobile dépasse les ventes de PC et offre donc des perspectives de marché de développement d’applications mobile très importantes.

Ces éditeurs doivent cependant s’adapter aux plateformes de développement mobile qui sont foncièrement différentes de celle des applications Desktop. En effet, ces plateformes imposent des contraintes fortes, les ressources étant limitées : taille d’écran réduite, processeur faible, stockage limité, connexion lente et intermittente, RAM limitée. Toutes ces limitations impliquent une discipline de développement particulière pour que l’expérience utilisateur soit toujours efficace.

Au niveau logiciel, de multiples OS (Android, iOS, BlackBerry, Windows Phone) se disputent le marché de la mobilité. A chaque OS correspond une plateforme de téléchargement d’applications, des outils de développement et un langage de programmation permettant le développement et la distribution des applications :

 


Cette hétérogénéité des outils de développement et des langages, rend difficile le développement d’applications mobile multiplateformes. Elle pousse les développeurs à faire un choix sur la plateforme, tout en assurant la plus grande distribution possible.

Des outils pour aider les développeurs ?

Google a récemment apporté un élément de réponse avec le projet J2ObjC qui permet de convertir du code Java (Android) en code Objective-C (iOS). L’intérêt d’une telle démarche est de pouvoir récupérer le code fonctionnel d’une application Android afin de faciliter la conversion vers la plateforme iOS.
Mais cette approche pose encore de nombreuses questions :
  • Qu’en est-il de la conversion Objective-C vers Java ? Et vers d’autres OS  ?
  • Et plus généralement, comment capitaliser sur le fonctionnel d’une application indépendamment des préoccupations techniques afin d’en faciliter la migration ?  

Une réponse avec le MDA

L’approche MDA a su faire ses preuves pour le développement d’applications d’entreprise et peut également apporter beaucoup pour les applications mobile. Au travers de 5 points, nous allons voir pourquoi et comment l’approche MDA peut nous aider à assurer la pérennité des savoir-faire, le gain de productivité tout en répondant aux problématiques de fragmentation des plateformes mobile.

1. Réduire les problématiques de fragmentation

Chaque OS mobile détient une part du marché, et donc chacun possède sa communauté d’utilisateurs et de développeurs. Cette fragmentation pose un souci pour les entreprises désireuses de toucher un large public car le développement des applications multiplateformes est très coûteux en temps et en argent. Les entreprises se trouvent face à un dilemme :
  • Développer une application de qualité en un temps efficace et limiter sa distribution à une plateforme spécifique. 
  • Développer une application aux fonctionnalités réduites, mais proposée sur plusieurs plateformes et à un large panel d'utilisateurs.
Des solutions hybrides existent comme Cordova (application native construite à partir de vue Web et Html5) ou Flex 4.6 (exécution sous plateforme Flash) et permettent d’assurer du multiplateformes, mais leur prise en charge reste techniquement partielle. En réalité, une application native restera plus adaptée pour plusieurs raisons :
  • L’utilisateur dispose d’un environnement adapté à sa plateforme et à ses spécificités : l’expérience utilisateur reste optimale.
  • Le développeur tire profit de l’architecture spécifique et optimise l’application en fonction de la plateforme.
  • L’application peut évoluer et éventuellement tirer parti des fonctionnalités spécifiques à une plateforme ou OS.

L’approche MDA peut répondre à ces problématiques : décrire les besoins fonctionnels d’une application indépendamment de la plateforme d’exécution, et respecter les spécificités techniques de chaque plateforme. MDA permet de générer le code sans valeur ajoutée (boilerplate code) spécifique à chaque plateforme. On assure ainsi un gain de productivité, même sur des applications natives.

2. Capitaliser sur les concepts proches

Chaque OS mobile utilise son propre langage de programmation et arbore ses propres APIs mais les concepts généraux restent standards et communs. On peut se permettre de capitaliser des concepts communs entre différentes plateformes permettant de modéliser une application de manière générique, citons par exemple :
  • Activité : correspond à une page de l’application, elle est une notion commune à tous les OS. Une application est composée d’une ou de plusieurs activités.
  • Gestion des données de l’application : en séparant le code de l’interface graphique et le code fonctionnel.
  • Vues et actions associées : une vue est un objet graphique par lequel l’utilisateur interagit avec l’application ce qui provoque une action liée à la vue. Nous retrouvons la plupart des vues sur tous les OS mobile : bouton, saisie de texte, label, barre de navigation, bouton radio, case à cocher, etc.
  • Type d’évènements : une application doit prendre en charge certains évènements susceptibles d’apparaitre durant son exécution tels que les appels téléphoniques, les SMS, etc.
  • Applications natives : les applications natives telles que le calendrier, les contacts, les alarmes, le lecteur média sont communs à tous les OS.
L’approche MDA divise la conception de l’application en deux points de vue :
  • Un point de vue "fonctionnel" indépendant des détails liés à la plateforme d’exécution. On définit ce que l’on veut générer (le quoi). Dans cette partie, on modélise des notions abstraites telles que des écrans, boutons, objets métier, DAO, mais en restant indépendant de leur implémentation technique. Modéliser le fonctionnel de l’application assure la pérennité des savoir-faire.
  • Un point de vue "technique" dans lequel sera décrit l’architecture de la plateforme d’exécution. On définit comment on veut le générer (le comment). Dans cette partie, on spécifie comment la notion fonctionnelle doit être générée : quel langage (Objective C, Java), quelle version (Android 4.1 par exemple), quelle implémentation de BDD. Modéliser l’architecture technique assure la prise en compte de la plateforme d’exécution.
L’étape qui suit la description fonctionnelle et technique d’une application est la génération du code source, c’est le rôle du générateur (ex. MIA Studio, Acceleo). Il s’appuie sur le modèle fonctionnel et technique de l’application pour générer du code source que le développeur pourra enrichir manuellement (le traitement algorithmique et les ressources qui ne peuvent pas être déduits du modèle). La modélisation nous assure une pérennisation du savoir fonctionnel. La génération de code assure un gain de temps considérable entrainant un gain de productivité.

3. Elargir l’écosystème mobile

L’écosystème - tout ce qui vit autour d’une plateforme pour en faciliter le développement - existant sous JavaEE/.NET est large, mais sur plateforme mobile, cet écosystème est encore réduit. L’utilisation de librairies tierces dans les applications mobile est limitée par les différents OS pour plusieurs raisons :
  • Une taille d’application importante : l’utilisation massive de librairies est au contraire des bonnes pratiques pour le développement mobile (problématique d’espace mémoire).
  • Corrolaire du point précédent : une taille d’application trop importante peut être un frein à son téléchargement (debit limité et frais des réseaux 3G).
  • Le manque de visibilité des développements des librairies. Alors que les librairies pour applications JEE/.Net sont portées par de grands éditeurs (JBoss, Spring, Microsoft, etc.), les librairies mobile existantes sont quasi-exclusivement développées par des développeurs tiers. Le suivi et la maintenance ne sont donc pas toujours assurés.
  • Les éditeurs Google, Apple, Microsoft gardent la main sur les évolutions des SDK et ne garantissent donc pas la compatibilité des librairies tierces avec les nouvelles versions des plateformes.
Pour assurer la pérennité du code et élargir son périmètre de compatibilité à plusieurs versions, il est donc préférable d’être indépendant au maximum des librairies tierces et d’avoir un code standard au SDK. L'approche MDA réduit le temps de développement en générant du code standard au SDK, en respectant les bonnes pratiques d’architecture et de qualité.

4. Industrialiser le développement

La mise en place d’une approche MDA, conjointement avec les différents composants d’une usine logicielle (construction, Intégration continue, analyse qualimétrique), industrialise le développement et assure une bonne qualité au code.
L’industrialisation avec le MDA nécessite plusieurs éléments clés :
  • La mise en place d’un POC par une équipe d’architectes logiciels. Ce POC servira de base de code attendu pour faciliter la réalisation du générateur.
  • Un code généré ne doit pas être source d’erreurs et doit rester compatible avec les niveaux de qualimétrie logicielle (qualité, architecture).
  • Une formation des développeurs à l’utilisation du DSL et du générateur.
  • Un support technique permettant d’accompagner les développeurs sur les outils afin d’assurer un bon démarrage du projet.
  • Une communauté autour du générateur qui remonte les bugs. L’amélioration en continue du générateur profite aux développements en cours et aux prochains projets.
  • Une capitalisation sur les bonnes pratiques de développement afin de développer des futures applications encore plus rapidement.
Sur le marché, il y a encore peu de développeurs avec des compétences “mobile”. L’industrialisation des développements mobile par l’approche MDA, a encore plus de sens dans ce contexte, car il permet d’avoir un meilleur accompagnement et un meilleur suivi.

5. Réduire le coût de possession

Avec des budgets toujours plus serrés, les entreprises veulent réduire le coût de possession de leurs applications. L’approche MDA, et plus spécifiquement le DSL et le générateur de code ont une grande valeur de capitalisation et de réutilisabilité. Elles permettent ainsi de réduire les coûts de possession, notamment sur l’investissement initial et le coût de maintenance  :
  • Une conception facilitée par un DSL adapté, cadrant les besoins spécifiques aux plateformes mobile. 
  • Un générateur de code disponible “sur étagère” et utilisable rapidement permet de booster le démarrage d’un projet
  • Réduction du support technique : le code ou les patterns complexes et qui sont enclins aux erreurs (typiquement liés à la persistance, mapping ORM) sont générés.
  • Un code de qualité suivant les bonnes pratiques assure une meilleure maîtrise et une réduction des coûts de maintenance
  • Un processus itératif : l’outillage MDA par amélioration continue va réduire les coûts des projets progressivement au fur et à mesure de son amélioration.
Un OS peut aujourd'hui être leader du marché et demain perdre certains marchés. Un exemple, avec les annonces de Samsung, premier fabricant mondial de smartphones, qui compte  sortir de nouveaux smartphones en 2013 en abandonnant Android pour Tizen. Ou encore à l’image du géant chinois Huawei qui propose son propre OS mobile ou encore plus récemment Firefox avec FirefoxOS.



En conclusion, au lieu d'investir sur la technologie qui abrite une application, mieux vaut investir dans une technique de développement qui permet de capitaliser la fonctionnalité d'une application indépendamment de sa technologie. Une méthode qui intègre l'évolutivité et la pérénité dans son processus de développement comme l'approche MDA.