Pourquoi tester
Intégrer des tests à notre code présente plusieurs avantages, notamment l'amélioration de sa qualité et de sa fiabilité. Cependant, la raison la plus essentielle réside dans le fait que les tests nous permettent d'avoir confiance lors de l'ajout de nouvelles fonctionnalités ou de la refactorisation, en garantissant le bon fonctionnement de notre application.
Les types de tests
Sans rentrer dans les détails, car ce n’est pas le but de l’article, il est important de mentionner les différents types de tests qui sont adaptés pour différents cas d’utilisation.
Tests statiques
Les tests statiques sont effectués sans nécessiter l’exécution du code. Cela permet de détecter très rapidement et à moindre coût les fautes de frappe, les typages incorrect, etc…
Le langage Dart étant typé, ce type d’erreur sera remonté très tôt dans votre IDE.
Un linter est fourni par défaut lors de l’installation du SDK, mais il ne permet pas de créer ses règles. D’autres librairies permettant cela existent cependant (custom_lint par exemple).
Tests unitaires
Comme leur nom l’indique ils permettent de tester unitairement une petite partie du code (une fonction, une classe, un widget…). Ils sont simples à mettre en œuvre et sont exécutés très rapidement.
Tests d’intégration
Les tests d’intégration permettent de valider que plusieurs widget, entités de code interagissent entre eux correctement.
Tests E2E
Les tests E2E (end-to-end) ou fonctionnels sont beaucoup plus poussés, ils simulent dans un environnement complet des actions utilisateurs. Ils permettent de tester des scénarios précis de l’application (authentification, consultation d’un solde, soumission d’un formulaire…).
Les golden tests, nous en reparlerons plus bas dans l’article, font parties des e2e. Il faut dans un premier temps lancer des tests avec des valeurs d’entrées qui nous permettront d’avoir un résultat de référence. Par la suite il suffira de comparer les résultats des nouveaux tests avec ceux de référence.
Tests manuels
Enfin, les tests manuels, qui peuvent être coûteux en temps mais permettent de valider humainement la livraison d’une nouvelle fonctionnalité ou prévenir d’une potentielle régression.
Mise en place de tests unitaires
Ecriture des tests
Nativement Flutter possède des outils pour réaliser des tests unitaires, il suffit d’ajouter le package flutter_test dans les dev_dependencies du fichier pubspec.yaml.
Par convention tous les fichiers de tests doivent être suffixés par _test.dart.
Les deux principales fonction fournies par le package sont les fonctions test et expect.
Ci dessous un exemple d’un fichier de test permettant de s’assurer que les fonctions de conversion Celsius vers Fahrenheit et Fahrenheit vers Celsius fonctionnent correctement.
La fonction expect prend en paramètre la valeur de que l’on souhaite tester et en second la valeur que doit prendre notre test pour réussir.
A noter qu’il est possible de regrouper des tests liés entre eux avec la méthode group.
Pour plus d’information sur le sujet vous pouvez vous référer au Cookbook Flutter.
Lancer les tests
L’exécution des tests se fait très simplement en lançant la méthode flutter test.
Générer un rapport de code coverage
Flutter propose de générer un rapport de couverture de codes nativement en ajoutant l’option --coverage à la commande flutter test. Un fichier lcov.info sera alors généré dans un dossier /coverage à la racine du projet.
Il faudra ensuite installer un outil type genhtml pour convertir notre fichier lcov.info en beau rapport HTML: genhtml coverage/lcov.info -ocoverage/report
Il manque actuellement la fonctionnalité de couverture de branche qui permet de valider que nos tests sont passés par chaque point de décision dans notre code (instruction conditionnelle “if” ou “switch”). Cependant l'équipe Flutter travaille sur ce sujet: https://github.com/dart-lang/coverage/issues/141
Mocking
Quand on fait des tests unitaires, l’objectif est de tester chaque partie du code pour s’assurer que chacune d’elle fonctionne comme elle le doit. Pour aller dans ce sens, une bonne pratique consiste à découper son code en plus petit bouts de manière à ce que chacun d’eux ait un rôle bien précis (voir le principe de responsabilité unique).
Prenons un exemple simple : je veux que mon code puisse faire un appel Http GET sur une api pour obtenir des informations à partir d’une data. L’image suivante représente la base de construction de notre code :
L’objectif est de pouvoir tester le comportement de notre data source qui fait appel à notre client Http, et le comportement de notre repository qui fait appel à notre data source.
Voici le code des différentes classes utilisées:
my_entity.dart
my_model.dart
data_source.dart
repository.dart
Il est important ici d’observer la construction de notre data source et de notre repository. En effet le data source est construit avec une instance de client http, et le repository est construit avec une instance de data source. Cette façon de construire ces deux classes va nous permettre de mocker le comportement des instances transmises aux classes que l’on veut tester. Un mock a pour rôle d'étendre une classe existante et de reproduire le comportement de cette dernière de manière contrôlée.
Maintenant que nous avons construit notre code à tester, il nous faut une solution pour réaliser le mocking de nos classes. Pour les tests Flutter, il existe deux plugins soutenus par la communauté : mockito et mocktail. Nous avons préféré utiliser mocktail car il présente les mêmes fonctionnalités que mockito sans avoir besoin de générer des fichiers.
La première chose à faire, c’est de tester notre data source en mockant notre client Http qui est en bout de chaine. Pour se faire, nous avons créé un fichier json correspondant à ce qui serait obtenu après décodage du body d’une response 200 :
Puis nous avons créé un helper qui nous permet de lire ce fichier, d’obtenir son contenu en String ou en Map :
Nous pouvons maintenant tester notre data source et mocker le comportement de notre client Http :
Le plus important ici est le mot clé when qui permet de définir le comportement que notre instance mockée doit adopter pendant le test. Dans le premier test on vérifie que notre data source renvoie bien une instance MyModel après avoir fait appel au client Http. Dans le deuxième test on vérifie que si le client http soulève une erreur, notre data source la soulèvera aussi pour la propager au reste du code.
Maintenant voici le test de notre repository :
La logique reste exactement la même : on veut mocker notre data source pour pouvoir créer un scénario de succès (avec l’obtention d’une instance de MyEntity) et un d’erreur qui soulèvera une exception.
Dans notre exploration nous avons réalisé des tests très simples du type OK & KO. On retiendra qu’il est possible de mocker une partie du code pour pouvoir tester celle qui nous intéresse en créant différents scénarios d’intérêt.
Mise en place de tests E2E
Environnement et contexte
Tout comme les tests unitaires, Flutter intègre nativement les tests E2E à la seule différence qu’en plus du package flutter_test dans les dev_dependencies du fichier pubspec.yaml il faut aussi ajouter integration_test.
Les tests doivent être rangés dans le répertoire integration_test/, et seront exécutés ceux suffixé par _test.dart (cette convention est décrite dans la documentation Integration testing de Flutter).
Ainsi, la suite de cet article décrit nos premiers pas dans le monde des tests E2E avec Flutter. Pour ce faire, nous avons décidé de tester l’application de compteur proposée par le package flutter_bloc.
Écriture des tests
Premièrement, nous avons définis nos scénarios : un test d’incrément et de décrément du compteur.
Dans chacun des cas le code teste :
· la présence d’un widget Text représentant le compteur et qu’il est initialisé à 0
· la présence du widget d’incrément ou de décrément (FloatingActionButton)
· la simulation de l’appui sur ce bouton
· la vérification du changement d'état du compteur, affichant 1 ou -1 en fonction de l’action précédente.
Dans le cas du test de la fonctionnalité de décrément, le code est le suivant :
Si l’on décompose cet exemple, on comprend que le test est joué dans un main. Il est également impératif d’ajouter la ligne 2 afin d’assurer sa correcte initialisation. Ligne 4, un group de test est instancié. La suite permet de tester les widgets de notre application en exécutant un testWidgets. Cette méthode offre le lancement de notre application (ligne 7) suivi des tests décrits.
Des informations détaillées sur le rôle méthodes de tests utilisées sont disponibles dans cette documentation Flutter.
Lancer les tests
Afin d'éviter de build une application à chaque exécutions d’un de nos fichiers de tests (fichier d’incrément et de décrément), nous avons organisé notre répertoire integration_test comme suit :
integretion_test/
|_ main_test.dart
|_ single_decrement.test.dart
|_ single_increment.test.dart
Seul main_test.dart sera exécuté pour lancer nos tests. Effectivement, ce dernier est codé afin de jouer le main de chacun des deux autres fichiers avec ce script :
De cette manière on build une application et nos tests sont joués les uns après les autres. Tests qui sont exécutés à partir de l’onglet Testing de Visual Studio Code :
Mise en place des tests golden
Les tests golden sont nativement pris en charge par le framework de test de Flutter.
Il suffit donc d’ajouter la dépendance au package flutter_test dans les dev_dependencies du fichier pubspec.yaml.
Écriture des tests
La gestion des golden tests est possible à travers la fonction matchesGoldenFile. Ce matcher est utilisé dans une méthode expectLater comme suit :
final finder = find.byIcon(Icons.add);
await expectLater(finder, matchesGoldenFile('goldens/vanilla/magical_button_unpressed.png'));
L’appel à la méthode matchesGoldenFile génère le fichier indiqué en paramètre.
Dans notre cas, nous obtenons le fichier suivant :
On remarque que les textes et icones de l’application sont remplacés par des placeholders par la librairie. En fait, Flutter choisi par défaut de remplacer toutes les fonts utilisées dans l’application par une font unique nommée “ahem”. Cette font embarque un seul et unique caractère, un carré, qui remplacera tous les caractères et icones affichés dans l’application testée.
Ceci a été mis en place pour éviter d’avoir des faux négatifs lorsque l’application est lancée sur des devices différents, le rendu d’une font pouvant varier.
Librairie golden_toolkit
Si vous souhaitez malgré tout embarquer les fonts dans vos tests, vous pouvez embarquer la librairie golden_toolkit qui offre cette possibilité.
La librairie permet d’afficher dans les tests les textes rendus avec les bonnes polices.
Cependant, d’une plateforme à l’autre, des différences peuvent apparaitre, il faudra donc utiliser cette option avec du recul.
Conclusion
En une après midi, nous avons pu mettre en place les différents types de tests existants sur Flutter, la plupart ne nécessitent pas d’ajouter de librairies spécifiques.
Nous avons notés quelques points perfectibles :
· l’algorithme de détection de la couverture de code n’est pas optimal,
· la gestion native des tests golden ne prend pas en compte les polices de caractère embarquées
Cependant, les tests sont simples à mettre en place et nous ont satisfaits.
A noter que, si nos tests ont montrés des performances acceptables, il est notoirement connu que l’exécution d’un grand nombre de tests nécessitera un certain temps et pourrait demander des optimisations sur le volume exécuté par exemple.
Simon Mahé – Lead Developer web / mobile
Simon Bernardin – Lead Developer Web / Mobile
Quentin Lebreton – Développeur Flutter
Guillaume Camanes – Lead Developer Flutter
Bastien CIVEL – Développeur web / mobile