Test des applications Flutter. Commencer

On se souvient de Flutter lorsqu'il est nécessaire de créer rapidement une application belle et réactive pour plusieurs plateformes à la fois, mais comment garantir la qualité du code «rapide»?
Vous serez surpris, mais Flutter a les moyens non seulement d'assurer la qualité du code, mais aussi de garantir l'opérabilité de l'interface visuelle.
Dans l'article, nous examinerons comment les choses se passent avec les tests sur Flutter, nous analyserons les tests de widgets et les tests d'intégration de l'application dans son ensemble.



J'ai commencé à étudier Flutter il y a plus d'un an, avant sa sortie officielle, au cours de l'étude, il n'a pas été difficile de trouver des informations sur le développement. Et quand j'ai voulu essayer TDD , il s'est avéré que les informations sur les tests étaient désastreusement petites. En russe, et en général, presque aucun. Les problèmes de test devaient être étudiés indépendamment, selon le code source des tests Flutter et des articles rares en anglais. Tout ce que j'ai étudié sur le test des éléments visuels, je l'ai décrit dans un article pour aider ceux qui commencent tout juste à approfondir le sujet.


Test des widgets


Informations générales


Un test de widget teste un seul widget. Il peut également être appelé test de composant. Le but du test est de prouver que l'interface utilisateur du widget ressemble et interagit comme prévu. Le test d'un widget nécessite un environnement de test qui fournit le contexte approprié pour le cycle de vie du widget.
Le widget testé a la capacité de recevoir des actions et des événements utilisateur et d'y répondre, de créer une arborescence de widgets enfants. Par conséquent, les tests de widgets sont plus complexes que les tests unitaires. Cependant, comme le test unitaire, l'environnement de test de widget est une simulation simple, beaucoup plus simple qu'un système d'interface utilisateur à part entière.


Les tests de widgets vous permettent d'isoler et de tester le comportement d'un seul élément de l'interface visuelle. Et, ce qui est remarquable, d'effectuer toutes les vérifications dans la console, ce qui est idéal pour les tests qui s'exécutent dans le cadre du processus CI / CD.


Les fichiers contenant des tests se trouvent généralement dans le sous-répertoire de test du projet.
Les tests peuvent être exécutés à partir de l'EDI ou de la console avec la commande:


$ flutter test 

Dans ce cas, tous les tests avec le masque * _test.dart du sous-répertoire test seront exécutés.
Vous pouvez exécuter un test distinct en spécifiant le nom du fichier:


 $ flutter test test/phone_screen_test.dart 

Le test est créé par la fonction testWidgets , qui reçoit un outil comme paramètre de testeur , avec lequel le code de test interagit avec le widget testé:


 testWidgets(' ', (WidgetTester tester) async { //   }); 

Pour combiner des tests en blocs logiques, les fonctions de test peuvent être combinées en groupes, à l'intérieur de la fonction de groupe :


 group('  ', (){ testWidgets(' ', (WidgetTester tester) async { //   }); testWidgets(' ', (WidgetTester tester) async { //   }); }); 

Les fonctions setUp et tearDown vous permettent d'exécuter du code «avant» et «après» chaque test. En conséquence, les fonctions setUpAll et tearDownAll vous permettent d'exécuter le code «avant» et «après» tous les tests, et si ces fonctions sont appelées à l'intérieur du groupe, elles seront appelées «avant» et «après» l'exécution de tous les tests du groupe:


 setUp(() { //    }); tearDown(() { //    }); 

Recherche de widgets


Afin d'effectuer une action sur un widget imbriqué, vous devez le trouver dans l'arborescence du widget. Pour ce faire, il existe un objet de recherche global qui vous permet de trouver des widgets:


  • dans l'arborescence par text - find.text , find.widgetWithText ;
  • par clé - find.byKey ;
  • par icône - find.byIcon , find.widgetWithIcon ;
  • par type - find.byType ;
  • par position dans l'arbre - find.descendant et find.ancestor ;
  • en utilisant une fonction qui analyse les widgets sur une liste - find.byWidgetPredicate .

Tester l'interaction du widget


La classe WidgetTester fournit des fonctions pour créer un widget de test, en attendant que son état change et pour effectuer certaines actions sur ces widgets.
Toute modification du widget entraîne une modification de son état. Mais l'environnement de test ne reconstruit pas le widget en même temps. Vous devez indiquer indépendamment à l'environnement de test que vous souhaitez reconstruire le widget en appelant les fonctions pump ou pumpAndSettle .


  • pumpWidget - créez un widget de test;
  • pompe - commence à traiter la transition d'état du widget et attend qu'elle se termine dans le délai spécifié (100 ms par défaut);
  • pumpAndSettle - appelle la pompe dans un cycle pour changer les états pendant un délai donné (100 ms par défaut), c'est l'attente pour que toutes les animations se terminent;
  • appuyez sur - envoyez un clic au widget;
  • longPress - appui long;
  • lancer - glisser / glisser;
  • glisser -transférer;
  • enterText - saisie de texte.

Les tests peuvent implémenter à la fois des scénarios positifs, en vérifiant les opportunités planifiées et des scénarios négatifs pour s'assurer qu'ils n'entraînent pas de conséquences fatales, par exemple, lorsqu'un utilisateur clique dans la mauvaise direction et n'entre pas ce qui est requis:


 await tester.enterText(find.byKey(Key('phoneField')), 'bla-bla-bla'); 

Après toute action avec des widgets, vous devez appeler tester.pumpAndSettle () pour changer les états.


Moki


Beaucoup connaissent la bibliothèque Mockito . Cette bibliothèque du monde Java s'est avérée si réussie qu'il existe des implémentations de cette bibliothèque dans de nombreux langages de programmation, y compris Dart.


Pour vous connecter, vous devez ajouter la dépendance au projet. Ajoutez les lignes suivantes au fichier pubspec.yaml :


 dependencies: mockito: any 

Et connectez-vous dans le fichier de test:


 import 'package:mockito/mockito.dart'; 

Cette bibliothèque vous permet de créer des classes moque, dont dépend le widget testé, afin que le test soit plus simple et ne couvre que le code que nous testons.
Par exemple, si nous testons le widget PhoneInputScreen , qui, lorsqu'il est cliqué, à l'aide du service AuthInteractor , effectue une demande au backend authInteractor.checkAccess () , puis en remplaçant la maquette au lieu du service, nous pouvons vérifier la chose la plus importante - le fait d'accéder à ce service.


Les mobs de dépendance sont créés en tant que descendants de la classe Mock et implémentent l'interface de dépendance:


 class AuthInteractorMock extends Mock implements AuthInteractor {} 

Une classe dans Dart est également une interface, il n'est donc pas nécessaire de déclarer l'interface séparément, comme dans certains autres langages de programmation.


Pour déterminer la fonctionnalité du mok, la fonction when est utilisée, ce qui vous permet de déterminer la réponse du mok à l'appel d'une fonction:


 when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.value(true)); 

Moki peut renvoyer des erreurs ou des données erronées:


 when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.error(UnknownHttpStatusCode(null))); 

Chèques


Pendant le test, vous pouvez vérifier les widgets à l'écran. Cela vous permet de vous assurer que le nouvel état de l'écran est correct en termes de visibilité des widgets souhaités:


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

Une fois le test terminé, vous pouvez également vérifier quelles méthodes de la classe mob ont été appelées pendant le test et combien de fois. Cela est nécessaire, par exemple, pour comprendre si telle ou telle donnée est demandée trop souvent, s'il y a des changements inutiles dans l'état de l'application:


 verify(appComponent.authInteractor).called(1); verify(authInteractor.checkAccess(any)).called(1); verifyNever(appComponent.profileInteractor); 

Débogage


Les tests sont effectués dans la console sans aucun graphique. Vous pouvez exécuter des tests en mode débogage et définir des points d'arrêt dans le code du widget.


Pour avoir une idée de ce qui se passe dans l'arborescence des widgets, vous pouvez utiliser la fonction debugDumpApp () , qui, lorsqu'elle est appelée dans le code de test, affiche la représentation textuelle de la hiérarchie de l'arborescence des widgets entière à un moment donné dans la console.


Pour comprendre comment le widget utilise moki, il existe une fonction logInvocations () . Il prend en paramètre une liste de moxas et envoie à la console une séquence d'appels de méthode pour ces moxas qui ont été effectués lors du test.


Un exemple d'une telle conclusion est ci-dessous. La marque VERIFIÉE se trouve sur les appels qui ont été vérifiés lors du test à l'aide de la fonction de vérification :


 AppComponentMock.sessionChangedInteractor [VERIFIED] AppComponentMock.authInteractor [VERIFIED] AuthInteractorMock.checkAccess(71111111111) 

La préparation


Toutes les dépendances doivent être soumises au widget testé sous la forme d'un mok:


 class SomeComponentMock extends Mock implements SomeComponent {} class AuthInteractorMock extends Mock implements AuthInteractor {} 

Le transfert des dépendances vers le composant testé doit être effectué d'une manière ou d'une autre acceptée dans votre application. Pour simplifier la narration, considérons un exemple où les dépendances sont transmises via le constructeur.


Dans l'exemple de code, PhoneInputScreen est un widget de test basé sur StatefulWidget enveloppé dans Scaffold . Il est créé dans un environnement de test à l'aide de la fonction pumpWidget () :


 await tester.pumpWidget(PhoneInputScreen(mock)); 

Cependant, un vrai widget peut utiliser l'alignement pour les widgets imbriqués, ce qui nécessite MediaQuery dans l'arborescence des widgets, il obtient probablement Navigator.of (context) pour la navigation, il est donc plus pratique d'envelopper le widget sous test dans MaterialApp ou CupertinoApp :


 await tester.pumpWidget( MaterialApp( home: PhoneInputScreen(mock), ), ); 

Après avoir créé un widget de test et après toute action avec celui-ci, vous devez appeler tester.pumpAndSettle () pour que l'environnement de test gère toutes les modifications de l'état du widget.


Tests d'intégration


Informations générales


Contrairement aux tests de widgets, le test d'intégration vérifie l'intégralité de l'application ou une grande partie de celle-ci. L'objectif du test d'intégration est de s'assurer que tous les widgets et services fonctionnent ensemble comme prévu. Le fonctionnement du test d'intégration peut être observé dans le simulateur ou sur l'écran de l'appareil. Cette méthode est un bon substitut aux tests manuels. De plus, des tests d'intégration peuvent être utilisés pour tester les performances des applications.


Le test d'intégration est généralement effectué sur un véritable appareil ou émulateur, tel que iOS Simulator ou Android Emulator.


Les fichiers contenant des tests d'intégration sont généralement situés dans le sous-répertoire test_driver du projet.


L'application est isolée du code du pilote de test et démarre après celui-ci. Le pilote de test vous permet de contrôler l'application pendant le test. Cela ressemble à ceci:


 import 'package:flutter_driver/driver_extension.dart'; import 'package:app_package_name/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); } 

Les tests sont exécutés à partir de la ligne de commande. Si le lancement de l'application cible est décrit dans le fichier app.dart et que le script de test s'appelle app_test.dart , la commande suivante suffit:


 $ flutter drive --target=test_driver/app.dart 

Si le script de test a un nom différent, vous devez le spécifier explicitement:


 $ flutter drive --target=test_driver/app.dart --driver=test_driver/home_test.dart 

Un test est créé par la fonction de test et groupé par la fonction de groupe .


 group('park-flutter app', () { // ,       FlutterDriver driver; //     setUpAll(() async { driver = await FlutterDriver.connect(); }); //     tearDownAll(() async { if (driver != null) { driver.close(); } }); test(' ', () async { //   }); test(' ', () async { //   }); } 

Cet exemple montre le code de création d'un pilote de test via lequel les tests interagissent avec l'application testée.


Interaction avec l'application testée


L'outil FlutterDriver interagit avec l'application de test via les méthodes suivantes:


  • appuyez sur - envoyez un clic au widget;
  • waitFor - attendez que le widget apparaisse à l'écran;
  • waitForAbsent - attendez que le widget disparaisse;
  • scroll et scrollIntoView , scrollUntilVisible - faites défiler l'écran jusqu'au décalage spécifié ou au widget souhaité;
  • enterText , getText - entrez du texte ou prenez le texte du widget;
  • capture d'écran - obtenez une capture d'écran;
  • requestData - interaction plus complexe via un appel de fonction à l'intérieur de l'application testée.

Il peut y avoir une situation où vous devez influencer l'état global de l'application à partir du code de test. Par exemple, pour simplifier le test d'intégration en remplaçant une partie des services au sein de l'application par moki. Dans l'application, vous pouvez spécifier un gestionnaire de demande, auquel vous pouvez accéder via un appel à driver.requestData ('certains paramètres') dans le code de test:


 void main() { Future<String> dataHandler(String msg) async { if (msg == "some param") { //       return 'some result'; } } enableFlutterDriverExtension(handler: dataHandler); app.main(); } 

Recherche de widgets


La recherche de widgets pendant les tests d'intégration avec l'objet de recherche globale diffère dans la composition des méthodes des fonctionnalités similaires dans les tests de widgets. Cependant, la signification générale ne change pratiquement pas:


  • dans l'arborescence par text - find.text , find.widgetWithText ;
  • par clé - find.byValueKey ;
  • par type - find.byType ;
  • à l'invite - find.byTooltip ;
  • par étiquette sémantique - find.bySemanticsLabel ;
  • par position dans l'arbre find.descendant et find.ancestor .

Conclusion


Nous avons cherché des moyens d'organiser le test d'une interface d'application écrite à l'aide de Flutter. Nous pouvons à la fois implémenter des tests pour vérifier que le code répond aux exigences des spécifications techniques et effectuer des tests avec cette tâche même. Parmi les lacunes notées des tests d'intégration - il n'y a aucun moyen d'interagir avec les dialogues système de la plate-forme. Mais, par exemple, les demandes d'autorisations peuvent être évitées en émettant des autorisations à partir de la ligne de commande au stade de l'installation de l'application, comme décrit dans ce ticket .


Cet article est le point de départ pour explorer une rubrique de test qui présente brièvement au lecteur comment fonctionne le test de l'interface utilisateur. Il ne sauvegarde pas la documentation de lecture, à partir de laquelle il est assez facile de découvrir comment fonctionne une classe ou une méthode particulière. Après tout, l'étude d'un nouveau sujet pour vous-même nécessite, tout d'abord, une compréhension de tous les processus en cours dans leur ensemble, sans trop de détails.

Source: https://habr.com/ru/post/fr468631/


All Articles