Flutter et la Pyramide des tests avec le pattern BLoC

Alexandre POICHET
9 min readNov 26, 2021

--

This article is also available in english.

Introduction

Flutter est encore considéré comme un framework jeune mais il connait l’importance des tests au sein de son écosystème. Les développeurs ont quelques outils à disposition pour tester leur code. Il faut les connaitre et bien les accorder. Sans oublier certaines librairies intéressantes qui peuvent apporter une plus value dans vos tests. Donc techniquement, les développeurs Flutter n’ont pas de raisons valables de faire l’impasse sur les tests.

Cependant, la mise en place de tests logiciel est souvent douloureuse. C’est un monde tellement vaste et semé d’embûches. On y retrouve différentes catégories et sous catégories allant du test unitaire jusqu’au test utilisateur en passant par les tests de charge et de performances… Alors comment s’y retrouver ? Comment choisir une stratégie efficace et s’y tenir ? Laissez moi vous aider.

Dans cet article nous allons restreindre le périmètre aux tests fonctionnels et automatisées : des tests souvent implémentés par les développeurs dans le cadre d’une nouvelle fonctionnalité, que l’on veut rejouer facilement et continuellement pour détecter une régression le plus tôt possible dans le cycle de développement.

Choisir sa stratégie

Pyramide des tests

Quand on développe une nouvelle fonctionnalité, on étudie en amont les différents scenarii possibles, allant du cas nominal aux cas d’erreur en passant par les cas alternatifs. Ce sont différents cas d’utilisations que l’on aimeraient retrouver dans ses tests. Mais à quel niveau ? Directement dans les tests utilisateurs ? Et comme toujours, l’exhaustivité a un prix. Si on veut automatiser la totalité des cas d’utilisations en test bout en bout (ndlr test utilisateurs) dans le cycle de développement, alors il faut les resources correspondantes pour les exécuter dans un temps raisonnable. Et plus le projet grossit, plus le lancement des tests prend du temps. On doit donc trouver un compromis, c’est ce que nous enseigne la pyramide des tests.

Pyramide des tests (figure 1 sur 9)
  • Test unitaire :

Ils représentent la base de la pyramide car ils sont nombreux et rapides à jouer/rejouer. On peut retrouver ici l’exhaustivité des différents cas d’utilisations mais avec un périmètre restreint en terme d’interactions, donc pas de valeur fonctionnelle intrinsèque. Concrètement, on va tester une règle métier bien isolée ou un algorithme bien défini. Pour finir, on considère que c’est un test en boite blanche car il est de bas niveau, cela nécessite de connaitre le code pour effectuer ce genre de test.

  • Test intégration :

Au stade intermédiaire, on a la possibilité de tester des appels entre service ou vérifier un assemblage de composant. On ne cherche pas à retrouver systématiquement de la valeur fonctionnelle mais que les interactions se fassent bien. Suivant le test, on peut dire qu’on réalise un test en boite grise car on connait une partie du code mais pas nécessairement la totalité.

  • Test interface utilisateur :

Au sommet de la pyramide, on retrouve les tests interface utilisateur qui ont vocation à tester une fonctionnalité de bout en bout tout en examinant la partie graphique (positionnement des éléments, représentation des images, présence de propriétés, etc…). En tant qu’utilisateur, on ne connait pas le détail du code derrière, on teste le résultat d’une fonctionnalité, c’est donc un test en boite noire. Malgré leur utilité, ils ne sont pas nombreux car ils sont couteux en temps d’exécution et difficile à maintenir de par leur couverture étendue, on cherchera donc à les rationaliser.

Gestion d’état avec le pattern BLoC

Flutter est un framework au paradigme déclaratif, il nous impose de reconstruire la vue ou une partie de la vue en fonction d’un état qui mute.

Formule d’obtention du rendu graphique en fonction d’un état mutable (figure 2 sur 9)

Suivant la taille et les fonctionnalités de votre application, on a parfois besoin de partager cet état au sein de plusieurs vues et au contraire isoler une partie bien précise avec un changement d’état restreint.

Et quid de la navigation ? Souvent, on veut juste rediriger l’utilisateur vers une nouvelle page et non redessiner celle ci pour préserver un historique de navigation plus facile à maintenir.

Schéma d‘état/navigation d’une application (figure 3 sur 9)

Dans le cadre d’une application complexe (nombre de fonctionnalités conséquentes), il apparait difficile de définir une architecture maintenable et évolutive simplement avec les outils du framework Flutter. Pour vous aider dans la gestion d’état de votre application, il existe pléthore de librairies.

Nous allons choisir la librairie BLoC. Ce pattern s’inscrit pleinement dans le schéma d’état/navigation représenté ci dessus. Il nous permet également de séparer confortablement la partie logique de la partie présentation. Ainsi, on va pouvoir catégoriser plus facilement nos tests et tenter de les hiérarchiser.

Appliquer la stratégie

Nous allons essayer d’appliquer ces principes par des exemples. Ci dessous un petit formulaire d’ajout de passagers très simpliste :

Formulaire d’ajout voyageur (figure 4 sur 9)
  • Un écran formulaire avec validation des champs
  • Un Cubit de gestion d’affichage du text en fonction du choix de la liste déroulante
  • Un Bloc de gestion des échanges avec le serveur.
  • Un écran de succès d’ajout du passagers
  • Des snack bar d’erreur en cas de retour erreur

Tests avec le framework BLoC

Ici on a un exemple de test pour confirmer que l’état renvoyé par le cubit de description voyageur dépend du type de voyageur sélectionné. Dans ce test unitaire, on peut se permettre de tester l’exhaustivité des cas de sélection.

Dans l’exemple suivant, on vérifie bien que, lors de l’ajout d’un nouveau voyageur et en fonction des réponses renvoyées par le repository, on émet les états correspondants :

Test widget et test de navigation

On peut tester le rendu en fonction des états disponibles avec des tests widgets. En effet, on simule un état en entrée et on vérifie le comportement des widgets en sortie.

On peut aussi réaliser des tests de navigation avec un test widget. A l’issue du test, on vérifie que l’écran initial ne figure plus dans l’arbre et que la redirection vers le nouvel écran s’effectue bien.

Des tests IHM grace aux tests goldens

Les tests Golden sont issues de la librairie golden_toolkit et permettent de comparer au pixel près la capture visuel d’un widget sous format png.

Ces tests sont très utiles mais couteux en termes d’exécution. On cherchera donc à les rationaliser ! Ils permettent de contrôler si la charte graphique est bien respectée et ils peuvent également vérifier l’accessibilité.

Dans cet exemple, on obtient des captures de l’écran principal.

Captures d’écran avec les tests goldens, de gauche à droite : écran standard / écran zoom multiplié par deux / écran représentation sémantique (figure 5 sur 9)

Mais qu’en est il des nombreux états intermédiaires ? Il y a par exemple une multitude de combinaisons possibles pour un formulaire. Pour vérifier que la vue s’adapte bien, doit on réaliser un test golden pour chaque déclinaison ? La réponse est non, il est préférable d’utiliser les tests widget que l’on a vue juste avant pour ces différents cas.

Aller plus loin avec des tests utilisateurs

On peut compléter, en bout de chaine, par des tests utilisateurs dit bout en bout. Cette fois, pas de simulation d’état, les couches logiques et présentation sont testées ensembles. On reproduit dans le code du test les gestes utilisateurs et on affirme que le résultat attendu est bien conforme. Il est souvent nécessaire de monter un environnement dédié pour exécuter ces tests qui prennent malheureusement beaucoup de temps.

Il existe une multitude d’outils permettant de réaliser ce genre de tests dont :

  • La librairie flutter_driver et les tests dit d’intégration avec un environnement dédié, disponible sur la doc officielle de Flutter.
  • Des logiciels d’automatisation de tests Android/iOS tel que Appium. L’intérêt ici est de pouvoir combiner l’exécution à un outil d’écriture de test comme Cucumber pour construire et valider ses tests à plusieurs en langage Gherkin. Vous trouverez de parfaits exemples dans cet article https://medium.com/@maxime.pontoire/tester-automatiquement-une-application-flutter-77e8aa471cb
  • Firebase Test Lab peut réaliser des tests utilisateurs sur de vraie dispositif et sans code à l’aide d’un robot.

Etant donné que ce sont les tests qui prennent le plus de temps et de ressources pour s’exécuter, il est parfois utile de moduler le nombre de ces tests. En effet, lors d’une phase de livraison, on peut jouer plus de tests pour couvrir un périmètre plus large et gagner en confiance avant de lancer la production.

Ci dessous un exemple complet d’un test end to end avec flutter_driver.

On a choisit d’automatiser le cas de test sur l’ajout d’un voyageur jeune car il représente la fonctionnalité la plus importante de notre application.

Résultats de la stratégie

Couverture de code

Résultats couverture de code avec lcov (figure 6 sur 9)

On voit qu’on a une excellente couverture de code, même si ce n’est qu’un indicateur.

Temps d’execution

La vitesse d’execution varie naturellement d’une machine à l’autre, il est donc difficile de fournir une étude qualitative. J’ai néanmoins tenté de réaliser une expérience crédible pour récolter des résultats présentés dans le graphique ci dessous.

Résultats graphiques du temps d’execution des tests (figure 7 sur 9)

Ces tests ont été réalisées sur un mac book pro M1 à l’aide d’un script. On execute 10 fois de suite tous les tests en aléatoire et on fait une moyenne sur les résultats obtenus. On ne prend pas en compte le temps nécessaire pour monter un environnement afin d’executer des tests de bout en bout (évalué à 30 secondes)

Remarque :

  • l’execution du test bout en bout prend plus de temps qu’un test golden (en moyenne).
  • Les tests widget nous font gagner beaucoup de temps par rapport au test golden.
  • Le temps d’execution des tests BLoCs est faible par rapport aux autres.

On arrive à un temps total d’execution de 8,5 secondes. Cependant, on pourrait descendre bien plus bas en executant des tests en parallèles.

Supposons que nous avons uniquement des tests bout en bout pour couvrir tous nos cas. On a quatre cas nominaux suivant les typologies de passagers et quelques cas d’erreur à couvrir. On peut évaluer le temps d’execution à plus de 15 secondes, et ça sans avoir le moindre test golden pour vérifier la partie graphique !

Dans cet exemple minimaliste, on estime un gain en secondes. Pour un projet d’envergure, on parle très vite en minutes. Et suivant le nombre d’execution dans la journée, on peut gagner plusieurs heures par jour…

Conclusion

La séparation de la logique et de la présentation nous a permit d’isoler des catégories de tests et de pouvoir les exécuter en parallèle dans le cycle de développement dit intégration continue.

Mais si on s’arrêtait là, il y aurait des manques importants dans notre stratégie de test. On a besoin d’automatiser des cas d’utilisation qui traversent ces deux couches et reproduisent au plus près les comportements utilisateurs.

Pyramide des tests adaptée à Flutter (figure 8 sur 9)

En somme, on voit qu’on a réussit à reproduire et automatiser une stratégie de test qui s’approche de la pyramide des tests.

On part de la base avec des tests rapides et par conséquent la possibilité d’obtenir l’exhaustivité des cas. Ce qui est une bonne chose car c’est à cet endroit qu’on a isolé la partie logique, équivalent à la partie métier de l’application.

En partie intermédiaire, on teste la réponse graphique aux différents états disponibles grâce aux test golden. Et grace aux tests widget, on va pouvoir tester tous les cas intermédiaires qui peuvent s’affranchir d’un test golden couteux en temps d’exécution, sans oublier les tests de navigation.

Pour finir, on choisit avec soin quels sont les tests utilisateurs bout en bout que l’on veut inclure dans notre intégration continue. Pour aider à la sélection, on peut se baser sur la loi de Pareto, prendre les 20% des cas qui produisent 80% des effets.

Ci dessous un arbre décisionnel pour tenter de vous aider dans la construction de vos tests Flutter avec le pattern BLoC.

Arbre décisionnel pour appliquer les tests Flutter avec le pattern BLoC (figure 9 sur 9)

Références

--

--