Une introduction au TDD dans Flutter

  • posté le
  • par ESENS

Le TDD, c'est quoi ?

Dans ce nouvel article technique, on vous propose de découvrir comment appliquer la méthode Test-Driven Development (TDD) à Flutter, le kit de développement logiciel d'interface utilisateur open-source créé par Google.

Aussi appelé Red, Green, refactor; le cycle de développement en Test-Driven Development (TDD) peut être représenté avec le schéma suivant :


En partant d’un nouveau projet, ou a minima d’une nouvelle étape de projet, le TDD nous dit qu’avant d’écrire une seule ligne de code, il faut d’abord penser au test qui viendra le valider.

Présenté de cette façon, la méthode n'apparait pas des plus logiques, mais tout l'intérêt du TDD est de savoir quand et où l'utiliser.

De manière générale, il est intéressant de tester les points d’entrée et de sortie d'une application en cours de développement, ses API par exemple, car les tests mettent en lumière des changements éventuels. Ça n'empêche pas les bugs et les dysfonctionnements, mais ça permet tout de même d'identifier ce qui ne fonctionne plus et où.

Autres avantages importants de la méthode TDD : les régressions sont atténuées (les nôtres, comme celles de tout autre collaborateur participant au projet) et le refactoring s'en trouve simplifié car il est plus facile d'identifier les parties du code qui ne fonctionnent plus.

D'autre part, afin de tester notre code, il convient de séparer nos interfaces et de rendre une partie du code abstrait dans le respect des principes écrits de l’architecture SOLID. Toujours un plus dans le cadre de la conduite d'un projet.

Après cette brève introduction aux bienfaits potentiels de la méthode TDD, voyons maintenant comment l'appliquer à Flutter.

Comment appliquer le TDD à Flutter


Le saviez-vous ? Lorsque vous créez un nouveau projet dans Flutter, le TDD est en fait déjà présent sous la forme d’une classe de test !


import 'package:flutter/material.dart';

import 'package:flutter_test/flutter_test.dart';

import 'package:tddxflutter/main.dart';

void main() {

  testWidgets('Counter increments smoke test', (WidgetTester tester) async {

    // Build our app and trigger a frame.

    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.

    expect(find.text('0'), findsOneWidget);

    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.

    await tester.tap(find.byIcon(Icons.add));

    await tester.pump();

    // Verify that our counter has incremented.

    expect(find.text('0'), findsNothing);

    expect(find.text('1'), findsOneWidget);

  });

}

Dans cet exemple, le test simule le rendu et les actions de notre widget. Étant donné qu’avec Flutter “tout est Widget”, les tests auront pour but de vérifier que nos widgets ont bien le comportement attendu et qu’ils s’affichent correctement. Cependant, comme nous allons le voir, tester des interfaces n’est pas toujours aisé.

On dénombre trois catégories de tests automatisables :

- Les tests unitaires
- Les test de Widget
- Les tests d'intégrations

Dans la suite de cet article, nous allons nous concentrer sur les tests unitaires qui permettent de tester une simple fonction, méthode ou classe, et qui sont relativement simples à mettre en place avec Flutter.


Une histoire de test unitaire

Red test, Green Test

Partons du postulat suivant :

- Tester une application qui récupère des données via une API

Imaginons que nous souhaitons récupérer une liste d’utilisateurs via un appel API vers le endpoint de notre choix, et que l’on récupère une réponse sous la forme d’un json. Plutôt basique, non ?

Dans ce cas, nous allons commencer par définir notre modèle d’utilisateur, ses champs et enfin les méthodes pour...

Ho ho ! Nous avons basculé sur le mode TDD !

Je voulais donc dire, nous allons écrire un fichier de test qui va se baser sur un modèle d’utilisateur et tester une méthode qui reçoit bien un json sous la forme attendue.



Créons un fichier labellisé get_user_test.dart qui s'attendait à recevoir 4 champs d’un utilisateur dont on a demandé les informations. 

Alors que nous définissons un utilisateur dans les conditions initiales du test, ce test ne pourra pas aboutir car, rappelez-vous, nous n’avons pas encore défini ce qu’est un utilisateur.

Le test échoue. Il est ROUGE.

Afin de passer à la prochaine étape de notre cycle, définissons maintenant un utilisateur avec une nouvelle classe UserModel :



Il ne nous reste plus qu'à importer ce modèle dans notre classe de test pour le valider :



Ici, on triche un peu car on ne teste que l’implémentation et la définition de notre modèle d'utilisateur UserModel correspond bien à ce qu’on a marqué. On ne test donc pas encore d’interface à proprement parler.

Le test réussit enfin. Il passe au VERT.


Nouveaux Tests, nouveaux problèmes

D’après les principes du TDD, nous devrions refactorer notre code, l’améliorer, supprimer les redondances. Mais pour un simple modèle, ce n’est pas la peine.

La prochaine étape sera de gérer une réponse en json.

Pour cela nous créons un nouveau test dans la même classe (il n’est pas indispensable de créer tous ces tests dans un même fichier, mais autant les regrouper lorsque l’on teste les mêmes objets) :



Nous testons donc si la réponse est bien un json avec les éléments demandés.

Étant donné que nous n’avons pas encore de connexion vers une API, mais que nous voulons tester la réponse json retournée, nous allons nous faciliter la vie avec un fichier json en guise de ressource de test :



Bien entendu, ce test sera ROUGE car nous n’avons pas défini de méthode fromJson dans notre modèle d’utilisateur. Rappelez-vous : test >  code > refacto.

Après quelques petites retouches, nous voici donc avec un UserModel qui implémente bien un nouveau constructeur. On en profite également pour retoucher légèrement le modèle en ajoutant un nouveau champ ‘profession’ :



Et hop ! 



A noter que la majorité des IDE intègrent un espace dédiés aux test, mais nous pouvons aussi lancer les tests via la commande flutter test :



Flutter va aller chercher les fichier “*_test” dans le dossier test pour les lancer un par un.

Il ne nous reste plus qu'à gérer l’appel API en lui-même !

Vous connaissez la musique, on commence évidemment par le test qui devrait appeler un endpoint http et retourner un json :



Évidemment, notre test comporte de nombreuses erreurs que l’on va s’empresser de corriger, et dans l’ordre :

- Ajoutons le package ‘http’ à nos dépendances afin de faciliter les tests et mock dont on se servira justement pour définir notre appel API :



- Créons la classe ApiProvider qui va prendre notre appel et le simuler en renvoyant notre fichier local en réponse :



- Vérifions à présent que le test fonctionne :



Et hop, ça passe au VERT !

Bien. Maintenant que nous avons un peu plus d’expérience avec le TDD, et si nous nous attaquions au test de widget ?


Une histoire de widget

Point initial

Avec nos bases en TDD fraichement aquises, il est maintenant grand temps de se jeter complètement à l'eau en testant les widgets.

En reprenant l’exemple du début de cet article, introduisons à présent la méthode testWidgets, puis pumpWidget, qui vont nous permettre de simuler l’appel du widget au sein de la classe de test :



Il existe par ailleurs plusieurs méthodes utilisables pour les testWidget.


Les Golden Tests

Nous allons ici utiliser une variante des tests unitaires, appelés Golden tests, qui seront appliqués à nos interfaces utilisateurs. 

C’est d'autant plus pertinent qu'il s'agit pour nous de tester les widgets responsables de l’affichage de divers éléments graphiques. 

L’idée d’un Golden test est de générer une image de notre interface à l’instant du test, et de la comparer à une ancienne version sauvegardée lors du dernier test réussi.

Cela permet, en théorie, d’éviter les régressions d’interfaces utilisateur et surtout d'éviter d'avoir à lancer l’application car tout est simulé.

Pour se faciliter la tâche, nous utiliserons le package golden_toolkit regroupant tous les outils nécessaires.

Restons en TDD et imaginons que l’on souhaite tester un widget affichant un texte. Évidemment, nous allons commencer par le test en question :



Quelques petites précisions :

- Afin de pouvoir reconnaître le texte, Flutter est obligé de les charger. Pour ce faire, on fera appel à la méthode loadAppFonts() car sans cela, Flutter remplacerait les caractères par des blocs de couleurs indiscernables.

- On initialise ensuite notre widget SimpleText via le pumpWidget en lui donnant le texte “Ce texte est Rouge” en argument.

- Enfin, on nommera notre Golden Test “simple_red_text” et celui-ci deviendra la nouvelle référence lorsqu’on lancera le test avec la commande que je vais expliquer par la suite.

A présent, lançons simplement les tests via flutter test :


flutter test –update-goldens


Vous remarquerez alors l'apparition d'un dossier “goldens” comportant une image de notre interface. Ouvrons-la :



Évidemment, on ne teste que l’affichage du widget et ce widget affiche seulement un texte de couleur rouge.

Afin de comprendre l’aspect “golden” du test on va légèrement changer le widget en variabilisant la couleur.

Cela nous donne :



Un petit peu de refactoring, on relance le test, cette fois sans fixer de nouvelle version “golden" :


flutter test


Malheureusement, notre test n’est pas valide et la nouvelle image de notre widget diffère de 0.24%. Une différence minime certes, mais une différence tout de même ! Une régression, donc.

De plus, le package a généré un dossier "failures" dans notre dossier de test contenant quatre images :



simple_red_text_isolateDiff.png nous affiche l’élément spécifique qui diffère.



simple_red_text_maskedDiff.png est le masque du fichier précédent au sens graphique.



simple_red_text_masterImage.png est la dernière image de référence, ou “Golden” dans notre contexte.



simple_red_text_testImage.png est l’image du widget testée par le package.



Comme notre test n'aboutit pas, il faut refactorer, corriger notre action.

Afin de garder le même comportement que précédemment, on va légèrement modifier le constructeur et inclure :



Avec ça, notre test devrait s'avérer correcte : 



En modifiant ainsi le comportement, le test s'affiche effectivement comme étant réussi mais nous n’avons pas encore testé si l'on peut effectivement afficher n’importe quelle couleur.

Vous connaissez la suite, on commence toujours par le test !



Avec ces nouveaux tests, on peut ajouter la mise à jour de la version golden :



Victoire ! Nous obtenons en effet un texte de couleur bleu !

A noter que les fichiers générés par les golden tests peuvent légèrement différer avec les versions successives de Flutter et selon les OS. A voir comment le package évolue mais pour le moment, afin de garder une certaine homogénéité, il vaut donc mieux toujours générer ses golden tests au sein d’un même environnement pour ne prendre aucun risque.

Conclusion

A première vue, l’approche TDD peut paraître assez lourde à implémenter. C'est effectivement une vision un peu différente du développement de code.

Cependant, d'après ma propre expérimentation, c'est une approche plus intuitive car la méthode aide d'avantage le développement des fonctionnalités souhaitées par le client et le cycle de test vers la refacto pour passer notre test au vert s'inscrit dans un cycle vertueux.

On a certes l’impression de tricher un peu au début en satisfaisant les conditions du test avec notre première itération, mais en passer par là nous permet de bien maitriser les bases du problème.

Pour conclure, je dirais donc que le TDD est un outil fort pratique mais à utiliser avec parcimonie et grande attention !


------------------------------

Article rédigé par Pierre, Développeur Extraordinaire fan de Flutter | Retrouvez tous nos articles tech sur le Blog !

Vous êtes à la recherche d'un nouveau challenge technique ? Rejoignez l'équipe ESENS en postulant à nos offres d'emploi !


PARTAGER CET ARTICLE