Test d'intégration pour des applications JFx
Dans ce post, on s’intéresse à une façon d’organiser des tests fonctionnels qui peuvent être considérés ensuite comme des tests d’intégration. Effectivement, nous considérons une application JFx pour laquelle nous sommes amputés de la couche de service. Nous n’avons que l’API de cette couche. Évidemment, depuis l’avènement des mocks, à partir d’APIs, nous pouvons simuler ce que l’on veut presque comme l’on veut.
Nous définissons ici deux use-cases dans les méthodes de tests dans la classe MainTest. Notre structure de tests est organisée de telle façon que les uses-cases pourront être réutilisés pour toutes les futures implémentations de notre couche de services sachant que pour le moment nous avons des tests fonctionnels qui s’exécutent grâce à notre couche de service mockée.
Le source de l’application présentée ici est disponible sur Github.
La couche de service (package model)
En deux mots, notre API service nous permet de gérer un ensemble d’utilisateur (ajouter, supprimer et récupérer tous les utilisateurs). On retrouve une interface UserService et une classe abstraite User permettant de définir ce qu’on attend d’un utilisateur. Nous ajoutons également une fabrique ServiceFactory.
Les vues JFx (package views)
Cette application est minimaliste. Nous arrivons d’abord sur un menu avec deux boutons (views.Menu): le premier nous permet de nous rediriger vers un écran (views.UserList) qui nous permettra de supprimer un utilisateur, le second permet de quitter l’application. L’écran de suppression est composé d’une listview et de deux boutons: un pour demander la suppression de l’utilisateur sélectionné dans la listview et l’autre permettant de revenir au menu principal.
Le contrôleur (package controller)
Rien d’extraordinaire ici, le contrôleur joue son rôle de chef d’orchestre.
Que sont ces Story-lines (Scénarios)?
Dans la classe MainTest, nous trouvons deux scénarios classiques dans deux méthodes @Test :
Un utilisateur lance l’application, clique sur le bouton delete a user, sélectionne un utilisateur dans la listview, clique sur le bouton delete puis finalement décide d’annuler son opération. La classe StoryLineSelectAUserAndCancelMock définit explicitement ce que nous attendons comme interactions avec un mock de type UserService. Dans le cas de l’utilisation d’une implémentation concrète de notre service utilisateurs, nous utilisons la classe StoryLineSelectAUserAndCancelImpl comme une classe qui initialise le service d’utilisateurs avec des données intéressantes pour le jeu de test concerné.
Un utilisateur lance l’application, clique sur le bouton delete a user, sélectionne un utilisateur dans la listview, clique sur le bouton delete et finalement confirme son choix. La classe StoryLineSelectAUserAndConfirmMock définit explicitement ce que nous attendons comme interactions avec une instance du type UserService (en particulier un mock de ce type). Dans le cas de l’utilisation d’une implémentation concrète de notre service utilisateurs, nous utilisons la classe StoryLineSelectAUserAndConfirmImpl comme une classe qui initialise le service d’utilisateurs avec des données intéressantes pour le jeu de test concerné.
Dans le package storylines.mocks, les deux classes implémentant l’interface StoryLine contiennent dans leur méthode exécute() les interactions attendues avec le mock de notre couche de service. Vous retrouverez ci-dessous la classe StoryLineSelectAUserAndConfirmMock détaillant ce qu’on attend d’un mock de type UserService pour ce scénario.
Chaque scénario sera associé à une méthode @Test par une fabrique de scénarios (voir dans le dossier test : storylines.mocks.StorylineFactoryMocks). Les constantes que nous voyons apparaître dans cette fabrique sont définies dans la classe MainTest et correspondent chacune à un nom de méthode de test.
Discussion sur la classe abstraite MainTest
Arrêtons nous un instant sur la classe MainTest. Les jeux de tests de cette classe sont conçus sous l’hypothèse qu’il suffit d’avoir une fabrique de scénarios et une fabrique de services pour être opérationnels. Ce qui est effectivement le cas si vous regardez attentivement les deux classes étendant la classe MainTest : MainTestMock et MainTestImpl.
Le cas de test click and cancel est défini en utilisant la librairie TestFx dans le but de simuler une interaction humaine via un robot.
Le cas de test click and confirm est défini de la même façon.
Finalement, nous trouvons quelques fonctions supplémentaires pour gérer le timing des animations.
Le point intéressant ici est que nous parvenons à définir et à exécuter des tests fonctionnels sans implémentation de la couche de service (via des mocks). Ces tests peuvent ensuite être considérés comme des tests d’intégration puisqu’en ayant une fabrique de scénarios et une fabrique de services pour une nouvelle implémentation, on peut alors créer une nouvelle classe de test en héritant directement de la classe MainTest. Les résultats doivent être conformes à ceux initialement prévus avec nos mocks. Cet exercice, que j’ai proposé à mes étudiants de Master, s’inscrit dans le cadre du découplage des tests fonctionnels et des implémentations lorsque les tests fonctionnels sont écrits sous forme de tests unitaires et qu’on vise à valider des implémentations d’une interface donnée.