Tests d'intégration Flutter - C'est facile

Je pense que beaucoup de gens connaissent déjà Flutter et, au moins par intérêt, ils ont lancé des applications simples dessus. Il est temps de s'assurer que tout fonctionne comme il le faut, et les tests d'intégration nous y aideront.


Les tests d'intégration sur Flutter sont écrits à l'aide du pilote Flutter, pour lequel il existe un tutoriel simple et intuitif sur le site officiel . Dans leur structure, ces tests sont similaires à Espresso du monde Android. Vous devez d'abord trouver les éléments de l'interface utilisateur à l'écran:


final SerializableFinder button = find.byValueKey("button"); 

puis effectuez quelques actions avec eux:


 driver = await FlutterDriver.connect(); ... await driver.tap(button); 

et vérifiez que les éléments d'interface utilisateur requis sont dans l'état souhaité:


 final SerializableFinder text = find.byValueKey("text"); expect(await driver.getText(text), "some text"); 

Avec un exemple simple, bien sûr, tout semble élémentaire. Mais avec la croissance de l'application testée et l'augmentation du nombre de tests, je ne veux pas dupliquer la recherche d'éléments d'interface utilisateur avant chaque test. De plus, vous devrez structurer ces éléments d'interface utilisateur, car il peut y avoir de nombreux écrans. Pour ce faire, rendez l'écriture des tests plus pratique.


Objets d'écran


Dans Android ( Kakao ), ce problème est résolu en regroupant les éléments de l'interface utilisateur de chaque écran dans Screen (Page-Object). Une approche similaire peut être appliquée ici, à l'exception que dans Flutter, pour effectuer des actions avec des éléments d'interface utilisateur, vous avez besoin non seulement du Finder (pour rechercher un élément d'interface utilisateur), mais aussi de FlutterDriver (pour effectuer une action), vous devez donc stocker un lien vers FlutterDriver dans Screen .


Pour définir chaque élément de l'interface utilisateur, nous ajoutons la classe DWidget (D - du mot Dart dans ce cas). Pour créer un DWidget aurez besoin d'un FlutterDriver , avec lequel les actions seront effectuées sur cet élément d'interface utilisateur, ainsi que d'un ValueKey , qui coïncide avec le ValueKey Flutter du widget de l'application avec laquelle nous voulons interagir:


 class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; DWidget(this._driver, dynamic valueKey) : _finder = find.byValueKey(valueKey); ... 

find.byValueKey(…) lors de la création manuelle de chaque DWidget , il est donc préférable de transmettre la valeur ValueKey au ValueKey , et le DWidget lui-même obtiendra le SerializableFinder souhaité. Il n'est pas non plus très pratique de transmettre manuellement FlutterDriver lors de la création de chaque DWidget , vous pouvez donc stocker FlutterDriver dans BaseScreen et le transférer vers DWidget , et ajouter une nouvelle méthode pour BaseScreen pour créer BaseScreen :


 abstract class BaseScreen { final FlutterDriver _driver; BaseScreen(this._driver); DWidget dWidget(dynamic key) => DWidget(_driver, key); ... 

Ainsi, il sera beaucoup plus facile de créer des classes Screens et d'y intégrer des éléments d'interface utilisateur:


 class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get button => dWidget('button'); DWidget get textField => dWidget('text_field'); ... } 

Se débarrasser d' await


Une autre chose pas très pratique lors de l'écriture de tests avec FlutterDriver est la nécessité d'ajouter await avant chaque action:


 await driver.tap(button); await driver.scrollUntilVisible(list, checkBox); await driver.tap(checkBox); await driver.tap(text); await driver.enterText("some text"); 

Oublier l' await est facile, et sans cela, les tests ne fonctionneront pas correctement, car les méthodes du driver renvoient Future<void> et lorsqu'elles sont appelées sans await sont exécutées jusqu'à la première await à l'intérieur de la méthode, et le reste de la méthode est «reporté à plus tard».


Vous pouvez TestAction ce TestAction en créant une TestAction qui « TestAction » Future afin que nous puissions attendre qu'une action soit terminée avant de passer à la suivante:


 typedef TestAction = Future<void> Function(); 

(essentiellement, TestAction est une fonction (ou lambda) qui renvoie un Future<void> )


Maintenant, vous pouvez facilement exécuter la séquence TestAction sans attente inutile:


 Future<void> runTestActions(Iterable<TestAction> actions) async { for (final action in actions) { await action(); } } 

Utilisation de TestAction dans un DWidget


DWidget utilisé pour interagir avec les éléments de l'interface utilisateur, et il sera très pratique si ces actions sont TestAction afin qu'elles puissent être utilisées dans la méthode runTestAction . Pour ce faire, la classe DWidget aura des méthodes d'action:


 class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; ... TestAction tap({Duration timeout}) => () => _driver.tap(_finder, timeout: timeout); TestAction setText(String text, {Duration timeout}) => () async { await _driver.tap(_finder, timeout: timeout); await _driver.enterText(text ?? "", timeout: timeout); }; ... } 

Vous pouvez maintenant écrire des tests comme suit:


 class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get field_1 => dWidget('field_1'); DWidget get field_2 => dWidget('field_2'); DWidget field2Variant(int i) => dWidget('variant_$i'); DWidget get result => dWidget('result'); } … final mainScreen = MainScreen(driver); await runTestActions([ mainScreen.result.hasText("summa = 0"), mainScreen.field_1.setText("3"), mainScreen.field_2.tap(), mainScreen.field2Variant(2).tap(), mainScreen.result.hasText("summa = 5"), ]); 

Si vous devez effectuer une action dans runTestActions qui n'est pas liée à DWidget , il vous suffit de créer un lambda qui renvoie un Future<void> :


 await runTestActions([ mainScreen.result.hasText("summa = 0"), () => driver.requestData("some_message"), () async => print("some_text"), mainScreen.field_1.setText("3"), ]); 

FlutterDriverHelper


FlutterDriver dispose de plusieurs méthodes pour interagir avec les éléments de l'interface utilisateur (pression, réception et saisie de texte, défilement, etc.) et pour ces méthodes, DWidget a des méthodes correspondantes qui renvoient TestAction .


Pour plus de commodité, tout le code décrit dans cet article est publié en tant FlutterDriverHelper bibliothèque FlutterDriverHelper sur pub.dev .


Pour faire défiler les listes dans lesquelles les éléments sont créés dynamiquement (par exemple, ListView.builder ), FlutterDriver a une méthode scrollUntilVisible :


 Future<void> scrollUntilVisible( SerializableFinder scrollable, SerializableFinder item, { double alignment = 0.0, double dxScroll = 0.0, double dyScroll = 0.0, Duration timeout, }) async { ... } 

Cette méthode fait défiler le widget déroulant dans la direction spécifiée jusqu'à ce que le widget d' item apparaisse à l'écran (ou jusqu'à ce qu'une timeout ). Afin de ne pas passer scrollable sur chaque scroll, la classe DScrollItem a été ajoutée, qui hérite d'un DWidget et représente un élément de liste. Il contient un lien vers scrollable , donc lors du défilement il ne reste plus qu'à spécifier dyScroll ou dxScroll :


 class SecondScreen extends BaseScreen { SecondScreen(FlutterDriver driver) : super(driver); DWidget get list => dWidget("list"); DScrollItem item(int index) => dScrollItem('item_$index', list); } ... final secondScreen = SecondScreen(driver); await runTestActions([ secondScreen.item(42).scrollUntilVisible(dyScroll: -300), ... ]); 

Pendant les tests, vous pouvez prendre des captures d'écran de l'application et FlutterDriverHelper possède un Screenshoter qui enregistre les captures d'écran dans le bon dossier avec l'heure actuelle et peut fonctionner avec TestAction .


Autres problèmes et leurs solutions


  • Je ne pouvais pas trouver un moyen standard de cliquer sur les boutons dans les boîtes de dialogue heure / date - je dois utiliser TestHooks . TestHooks peuvent également être utiles pour changer l'heure / la date actuelle pendant le test.
  • Dans la liste déroulante de DropdownButtonFormField vous devez spécifier la key non pas pour DropdownMenuItem , mais pour l' child ce DropdownMenuItem , sinon le Flutter Driver ne pourra pas le trouver. De plus, le défilement dans la liste déroulante ne fonctionne pas encore ( problème sur github.com ).
  • la méthode FlutterDriver.getCenter renvoie Future<DriverOffset> , mais DriverOffset ne DriverOffset pas partie de l'API publique ( problème sur github.com )
  • Il y a quelques choses plus problématiques et pas évidentes qui existent déjà. Vous pouvez les lire dans un merveilleux article . La possibilité d'exécuter des tests sur le bureau et de réinitialiser l'état de l'application avant le début de chaque test était particulièrement utile.
  • Vous pouvez exécuter des tests à l'aide des actions Github. Plus de détails ici .

Todo


Les futurs FlutterDriverHelper pour FlutterDriverHelper comprennent:


  • faites défiler automatiquement jusqu'à l'élément de liste souhaité, si au moment d'y accéder, il n'est pas visible à l'écran (comme cela se fait dans la bibliothèque Kaspresso pour Android). Si possible, alors même dans les deux sens.
  • intercepteurs pour les actions effectuées avec un Dwidget ou DscrollItem .

Les commentaires et les commentaires constructifs sont les bienvenus.


Mise à jour (15/01/2020) : dans la version 1.1.0, TestAction est devenue une classe, avec le champ String name . Et grâce à cela, la journalisation de toutes les actions effectuées dans la méthode runTestActions été runTestActions .

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


All Articles