Tutoriel pour apprendre à élaborer un jeu de données avec l'outil JavaBean Marshaller

| Antoine Rey | Winjerome |

Les jeux de données font partie intégrante des tests. Élaborer un jeu de données demande une connaissance fonctionnelle, aussi bien sur la nature des données que sur le scénario de test envisagé. Utiliser des jeux de données réalistes participe à la compréhension du scénario de test et donc, à sa documentation.

S'il vous était possible de générer ces fameux jeux de données, seriez-vous intéressés ? C'est précisément l'objet de ce tutoriel et d'un modeste outil baptisé JavaBean Marshaller.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Tutoriel pour apprendre à élaborer un jeu de données avec l'outil JavaBean Marshaller

- Auteurs : Antoine Rey, Winjerome

- Parution : 15 septembre 2016

- Licence : Copyright ® 2016 Antoine Rey. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Contexte

- II. JavaBean Marshaller

- III. Fonctionnalités

- IV. Extensible

- V. Le fonctionnement

- VI. Conclusion

- VII. Remerciements

I - Contexte

Au cours de mes missions, j'ai rencontré plusieurs outils maisons de génération de jeux de données (souvent abusivement appelés mocks). Leur fonctionnement consiste à pouvoir sauvegarder des grappes d'objets au format XML ou JSON, puis à les recharger. Ces jeux de données sont utilisés dans des tests ou pour bouchonner des adhérences indisponibles.

Le déclenchement de la sauvegarde des jeux de données peut-être réalisé en AOP.

Forts pratiques, ces outils souffrent malgré tout de certaines limitations :

- relative lenteur de rechargement des fichiers XML et JSON, en particulier pour des tests unitaires dont l'exécution doit rester rapide ;

- impossibilité de factoriser des morceaux de jeux de données entre plusieurs tests ;

- maintenance et refactoring rendus difficiles ;

- complexité des graphes d'objets cycliques (IDREF en XML ou XStream JSON) ;

- complexité des relations bidirectionnelles ;

- format des jeux de données couplé à une technologie de marshalling (ex : JAXB, Jackson).

Partant de ce constat, je me suis demandé s'il était possible de remédier à ces limitations. La solution qui m'est instantanément venue à l'esprit fut d'utiliser un autre format que le XML ou le JSON, à savoir le Java. Après tout, lorsque l'on écrit manuellement des jeux de données, c'est en Java qu'on le fait (sauf si on passe par des outils tels que DbUnit). Le langage Java reste ce qu'il y a de plus naturel pour des développeurs Java. Qui plus est, vérifiés à la compilation, les jeux de données seront simples à maintenir.

II - JavaBean Marshaller

Disponible en Open Source sur github, le projet JavaBean Marshaller fournit la classe utilitaire JavaBeanMarshaller. En paramètre de la méthode generateJavaCode, vous passez l'objet racine de votre grappe d'objets Java. En sortie, une classe Java permettant de ré-instancier votre grappe sera créée.

Un exemple sera bien plus parlant. Prenons le diagramme de classes ci-dessous.

Non représentés sur ce diagramme, les classes Album et Artiste possèdent des getter / setter et constructeur sans argument.

Imaginons l'instance d'une classe Artist référençant un seul et unique Album.

Voici le bout de code correspondant et volontairement très compact :

1. Artist u2 = new Artist(1, "U2", ArtistType.GROUP);

2. Album joshuaTree = new Album("The Joshua Tree", LocalDate.of(1987, Month.MARCH, 9), u2);

3. u2.getAlbums().add(joshuaTree);

Ajoutons la dépendance maven :

1. <dependency>

2. <groupId>com.javaetmoi.util</groupId>

3. <artifactId>javaetmoi-javabean-marshaller</artifactId>

4. <version>1.0.1</version>

5. </dependency>

Appelons ensuite la méthode JavaBeanMarshaller.generateJavaCode(u2).

Une classe ArtistFactory contenant le code suivant est générée :

1. import com.javaetmoi.javabean.domain.Album;

2. import com.javaetmoi.javabean.domain.Artist;

3. import com.javaetmoi.javabean.domain.ArtistType;

4. import java.time.LocalDate;

5. import java.time.Month;

6. import java.util.ArrayList;

7. import java.util.List;

9. public class ArtistFactory {

10. public static Artist newArtist() {

11. Artist artist1 = new Artist();

12. List<Album> albums1 = new ArrayList<>();

13. Album album1 = new Album();

14. album1.setArtist(artist1);

15. album1.setReleaseDate(LocalDate.of(1987, Month.MARCH, 9));

16. album1.setName("The Joshua Tree");

17. albums1.add(album1);

18. artist1.setAlbums(albums1);

19. artist1.setName("U2");

20. artist1.setId(1L);

21. artist1.setType(ArtistType.GROUP);

22. return artist1;

23. }

24. }

Cette classe compile. Plus verbeuse que le code original, elle a le mérite de structurer la création d'une instance et présente un code lisible par tout développeur Java.

En fonction de vos cas de test, libre à vous de modifier les valeurs, d'ajouter d'autres albums…

III - Fonctionnalités

Dans sa version 1.0.0, le projet JavaBean marshaller supporte :

- JDK 7 et 8 ;

- collections ;

- Maps ;

- tableaux à 1 et 2 dimensions ;

- relations unidirectionnelles et bidirectionnelles ;

- graphe cyclique ;

- nombreux types du JDK :

- types primitifs : boolean, short, float,

- types wrappers : Boolean, String, BigDecimal,

- énumérations,

- date : util.Date, java.sql.Date, Calendar, XMLGregorianCalendar,

- Java 8 Date & Time API : LocalDate, Period, Instant…

- bibliothèques tierces :

- JodaTime : DateTime, Period, Instant…

IV - Extensible

Ouvert aux extensions, le générateur JavaBean marshaller permet d'enrichir les types supportés :

- autres classes du JDK ;

- classes de frameworks tiers ;

- classes d'une application métier.

Le mécanisme d'extension repose sur l'interface CodeGenerator. Implémenter cette interface puis enregistrer l'instance auprès du JavaBeanMarshaller suffisent. La classe abstraite DefaultCodeGenerator allège l'implémentation.

Repartons de l'exemple précédent. Essayons de modifier le code généré. Au lieu de faire appel au constructeur sans argument de la classe Album, on souhaite utiliser le constructeur prenant en paramètres ses propriétés.

Pour cela, on crée une classe AlbumGenerator implémentant la DefaultCodeGenerator.

1. public class AlbumGenerator extends DefaultCodeGenerator<Album> {

3. @Override

4. public void generateSetter(MethodSpec.Builder method, SetterParam param) {

5. Album album = getValue(param);

6. Item releaseDate = param.getMarshaller().buildItem(album.getReleaseDate());

7. String artistVarName = param.getMarshaller().getVariableName(album.getArtist());

8. method.addStatement("$L.add(new $T(\"$L\", "+releaseDate.getPattern()+", $L))", param.getVarName(), param.getValueClass(), album.getName(), releaseDate.getVal(), artistVarName);

9. }

10. }

Comme vous pouvez le constater, l'implémentation de la méthode generateSetter demande à manier l'API du générateur. Les tokens et et la classe MethodSpec.Builder viennent du framework Javapoet sur lequel le générateur s'appuie. Nous en reparlerons dans le chapitre suivant.

Les nombreuses implémentations existantes peuvent servir de documentation.

En reprenant la même grappe et en utilisant l'AlbumGenerator :

1. Artist u2 = new Artist(1, "U2", ArtistType.GROUP);

2. Album joshuaTree = new Album("The Joshua Tree", LocalDate.of(1987, Month.MARCH, 9), u2);

3. u2.getAlbums().add(joshuaTree);

4. JavaBeanMarshaller.register(new AlbumGenerator());

5. JavaBeanMarshaller.generateJavaCode(u2);

La méthode newArtist gagne en concision :

1. public static Artist newArtist() {

2. Artist artist1 = new Artist();

3. List<Album> albums1 = new ArrayList<>();

4. albums1.add(new Album("The Joshua Tree", LocalDate.of(1987, 3, 9), artist1));

5. artist1.setAlbums(albums1);

6. artist1.setName("U2");

7. artist1.setId(1L);

8. artist1.setType(ArtistType.GROUP);

9. return artist1;

10. }

V - Le fonctionnement

En interne, la classe JavaBeanMarshaller effectue un parcours de graphe. Pour y arriver, elle s'appuie sur la classe PropertyUtils de Commons BeanUtils. Les propriétés déjà traitées sont mémorisées dans un Set.

La génération de code Java s'appuie sur la bibliothèque Javapoet créée par Square. Bien que le code généré soit simple et aurait pu être fait sans bibliothèque tierce, Javapoet apporte :

1. la gestion de l'import des packages ;

2. l'ajout des ; à la fin de chaque instruction ;

3. une syntaxe inspirée de String.format() permettant d'éviter la concaténation de chaînes de caractères (tokens $T, $L et $N).

Un outil de génération de jeux de données facilitant les tests avait le devoir d'être testé. C'est chose faite. Sa couverture de test est accessible sur Coveralls.io.

Les tests unitaires reposent tous sur la même stratégie :

1. création d'une grappe d'objets ;

2. appel à la méthode generateJavaCode chargée de générer la classe Java ;

3. compilation de la classe (via la méthode getSystemJavaCompiler du JDK) ;

4. appel à la méthode statique permettant de recréer la grappe d'objets ;

5. comparaison des deux grappes d'objets (méthode assertReflectionEqual de Unitils).

VI - Conclusion

Dans ce billet, vous avez fait la connaissance avec un tout jeune générateur de Dataset Java (sa première version date du 19 mars 2016). Je compte sur vous pour me confirmer (ou non) son utilité. N'hésitez pas non plus à me soumettre les cas que je n'ai pas prévus. Et si vous souhaitez y contribuer, vous êtes les bienvenus.

Une prochaine étape consistera à rendre ce générateur moins intrusif. En effet, une fois le dataset généré, il faut bien penser à retirer du code de production l'appel au JavaBeanMarshaller ainsi que la dépendance maven. L'utilisation d'un agent et d'une annotation serait une solution.

VII - Remerciements

Cet article a été publié avec l'aimable autorisation d'Antoine Rey. L'article original (Génération de jeux de données Java) a été rédigé par Antoine Rey.

Nous tenons à remercier jacques_jean et Winjerome pour la relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.