Flutter wird in Erinnerung behalten, wenn es notwendig ist, schnell eine schöne und reaktionsschnelle Anwendung für mehrere Plattformen gleichzeitig zu erstellen, aber wie kann die Qualität des „schnellen“ Codes garantiert werden?
Sie werden überrascht sein, aber Flutter hat die Möglichkeit, nicht nur die Qualität des Codes sicherzustellen, sondern auch die Funktionsfähigkeit der visuellen Oberfläche zu gewährleisten.
In dem Artikel werden wir untersuchen, wie es mit Tests auf Flutter läuft, wir werden Widget-Tests und Integrationstests der gesamten Anwendung analysieren.

Ich habe vor mehr als einem Jahr vor seiner offiziellen Veröffentlichung angefangen, Flutter zu studieren. Während der Studie war es kein Problem, Entwicklungsinformationen zu finden. Und als ich TDD ausprobieren wollte, stellte sich heraus, dass die Informationen zum Testen katastrophal klein waren. Auf Russisch und im Allgemeinen fast keine. Testprobleme mussten gemäß dem Quellcode von Fluttertests und seltenen Artikeln in englischer Sprache unabhängig untersucht werden. Alles, was ich zum Testen visueller Elemente studiert habe, habe ich in einem Artikel beschrieben, um denjenigen zu helfen, die gerade erst anfangen, sich mit dem Thema zu beschäftigen.
Allgemeine Informationen
Ein Widget-Test testet ein einzelnes Widget. Es kann auch als Komponententest bezeichnet werden. Mit dem Test soll nachgewiesen werden, dass die Benutzeroberfläche des Widgets wie geplant aussieht und interagiert. Das Testen eines Widgets erfordert eine Testumgebung, die den geeigneten Kontext für den Lebenszyklus des Widgets bietet.
Das getestete Widget kann Benutzeraktionen und -ereignisse empfangen und darauf reagieren und einen Baum untergeordneter Widgets erstellen. Daher sind Widget-Tests komplexer als Unit-Tests. Wie der Unit-Test ist auch die Widget-Testumgebung eine einfache Simulation, die viel einfacher ist als ein vollwertiges Benutzeroberflächensystem.
Mit Widget-Tests können Sie das Verhalten eines einzelnen Elements der visuellen Oberfläche isolieren und testen. Und was bemerkenswert ist, alle Überprüfungen in der Konsole durchzuführen, was ideal für Tests ist, die als Teil des CI / CD-Prozesses ausgeführt werden.
Dateien, die Tests enthalten, befinden sich normalerweise im Test- Unterverzeichnis des Projekts.
Tests können entweder über die IDE oder über die Konsole mit dem folgenden Befehl ausgeführt werden:
$ flutter test
In diesem Fall werden alle Tests mit der Maske * _test.dart aus dem Unterverzeichnis test ausgeführt.
Sie können einen separaten Test ausführen, indem Sie den Dateinamen angeben:
$ flutter test test/phone_screen_test.dart
Der Test wird von der Funktion testWidgets erstellt , die ein Tool als Testerparameter empfängt, mit dem der Testcode mit dem zu testenden Widget interagiert:
testWidgets(' ', (WidgetTester tester) async {
Um Tests zu logischen Blöcken zu kombinieren, können Testfunktionen innerhalb der Gruppenfunktion zu Gruppen zusammengefasst werden:
group(' ', (){ testWidgets(' ', (WidgetTester tester) async {
Mit den Funktionen setUp und tearDown können Sie Code "vor" und "nach" jedem Test ausführen. Dementsprechend können Sie mit den Funktionen setUpAll und tearDownAll den Code "vor" und "nach" allen Tests ausführen. Wenn diese Funktionen innerhalb der Gruppe aufgerufen werden, werden sie "vor" und "nach" der Ausführung aller Tests in der Gruppe aufgerufen:
setUp(() {
Widget-Suche
Um eine Aktion für ein verschachteltes Widget auszuführen, müssen Sie sie im Widget-Baum finden. Zu diesem Zweck gibt es ein globales Suchobjekt, mit dem Sie Widgets finden können:
- im Baum nach Text - find.text , find.widgetWithText ;
- per Schlüssel - find.byKey ;
- durch icon - find.byIcon , find.widgetWithIcon ;
- nach Typ - find.byType ;
- nach Position im Baum - find.descendant und find.ancestor ;
- Verwenden einer Funktion, die Widgets in einer Liste analysiert - find.byWidgetPredicate .
Testen Sie die Widget-Interaktion
Die WidgetTester- Klasse bietet Funktionen zum Erstellen eines Test-Widgets, zum Warten auf eine Änderung des Status und zum Ausführen einiger Aktionen für diese Widgets.
Jede Änderung im Widget führt zu einer Änderung des Status. Die Testumgebung erstellt das Widget jedoch nicht gleichzeitig neu. Sie müssen der Testumgebung unabhängig angeben, dass Sie das Widget neu erstellen möchten, indem Sie die Funktionen pump oder pumpAndSettle aufrufen .
- pumpWidget - Erstellt ein Test-Widget.
- pump - Startet die Verarbeitung des Statusübergangs des Widgets und wartet, bis dieser innerhalb des angegebenen Zeitlimits (standardmäßig 100 ms) abgeschlossen ist.
- pumpAndSettle - Ruft pump in einem Zyklus auf, um den Status während eines bestimmten Timeouts zu ändern (standardmäßig 100 ms). Dies ist die Wartezeit, bis alle Animationen abgeschlossen sind.
- Tippen Sie auf - Senden Sie einen Klick an das Widget.
- longPress - langes Drücken;
- schleudern - wischen / wischen;
- Drag - Transfer;
- enterText - Texteingabe.
Tests können sowohl positive als auch negative Szenarien implementieren, um geplante Opportunities zu überprüfen und sicherzustellen, dass sie keine fatalen Folgen haben, z. B. wenn ein Benutzer in die falsche Richtung klickt und nicht das eingibt, was erforderlich ist:
await tester.enterText(find.byKey(Key('phoneField')), 'bla-bla-bla');
Nach allen Aktionen mit Widgets müssen Sie tester.pumpAndSettle () aufrufen, um den Status zu ändern.
Moki
Viele kennen die Mockito- Bibliothek. Diese Bibliothek aus der Java-Welt erwies sich als so erfolgreich, dass es Implementierungen dieser Bibliothek in vielen Programmiersprachen gibt, einschließlich Dart.
Um eine Verbindung herzustellen, müssen Sie die Abhängigkeit zum Projekt hinzufügen. Fügen Sie der Datei pubspec.yaml die folgenden Zeilen hinzu :
dependencies: mockito: any
Und verbinden Sie sich in der Testdatei:
import 'package:mockito/mockito.dart';
Mit dieser Bibliothek können Sie Moque-Klassen erstellen, von denen das getestete Widget abhängt, sodass der Test einfacher ist und nur den Code abdeckt, den wir testen.
Wenn wir beispielsweise das PhoneInputScreen- Widget testen, das beim Klicken mit dem AuthInteractor- Dienst eine Anforderung an das Backend authInteractor.checkAccess () ausführt und dann den Mock anstelle des Dienstes ersetzt, können wir das Wichtigste überprüfen - die Tatsache, dass auf diesen Dienst zugegriffen wird.
Abhängigkeitsmobs werden als Nachkommen der Mock- Klasse erstellt und implementieren die Abhängigkeitsschnittstelle:
class AuthInteractorMock extends Mock implements AuthInteractor {}
Eine Klasse in Dart ist auch eine Schnittstelle, sodass die Schnittstelle nicht wie in einigen anderen Programmiersprachen separat deklariert werden muss.
Um die Funktionalität des Mok zu bestimmen, wird die Funktion when verwendet, mit der Sie die Reaktion des Mok auf einen Aufruf einer bestimmten Funktion bestimmen können:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.value(true));
Moki kann Fehler oder fehlerhafte Daten zurückgeben:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.error(UnknownHttpStatusCode(null)));
Schecks
Während des Tests können Sie auf dem Bildschirm nach Widgets suchen. Auf diese Weise können Sie sicherstellen, dass der neue Status des Bildschirms in Bezug auf die Sichtbarkeit der gewünschten Widgets korrekt ist:
expect(find.text(' '), findsOneWidget); expect(find.text(' '), findsNothing);
Nach Abschluss des Tests können Sie auch überprüfen, welche Methoden der Mob-Klasse während des Tests wie oft aufgerufen wurden. Dies ist beispielsweise erforderlich, um zu verstehen, ob diese oder jene Daten zu oft angefordert werden und ob sich der Anwendungsstatus unnötig ändert:
verify(appComponent.authInteractor).called(1); verify(authInteractor.checkAccess(any)).called(1); verifyNever(appComponent.profileInteractor);
Debuggen
Tests werden in der Konsole ohne Grafiken durchgeführt. Sie können Tests im Debug-Modus ausführen und Haltepunkte im Widget-Code festlegen.
Um eine Vorstellung davon zu bekommen, was im Widget-Baum passiert, können Sie die Funktion debugDumpApp () verwenden, die beim Aufruf im Testcode die Textdarstellung der Hierarchie des gesamten Widget-Baums zu einem bestimmten Zeitpunkt in der Konsole anzeigt.
Um zu verstehen, wie das Widget Moki verwendet, gibt es eine logInvocations () -Funktion. Als Parameter wird eine Liste von Moxas verwendet und eine Folge von Methodenaufrufen für diese Moxas, die im Test ausgeführt wurden, an die Konsole ausgegeben.
Ein Beispiel für eine solche Schlussfolgerung ist unten. Die Markierung VERIFIED steht für Anrufe, die im Test mit der Überprüfungsfunktion überprüft wurden :
AppComponentMock.sessionChangedInteractor [VERIFIED] AppComponentMock.authInteractor [VERIFIED] AuthInteractorMock.checkAccess(71111111111)
Vorbereitung
Alle Abhängigkeiten sollten in Form eines Moks an das getestete Widget gesendet werden:
class SomeComponentMock extends Mock implements SomeComponent {} class AuthInteractorMock extends Mock implements AuthInteractor {}
Die Übertragung von Abhängigkeiten auf die getestete Komponente sollte in einer Weise erfolgen, die in Ihrer Anwendung akzeptiert wird. Betrachten Sie zur Vereinfachung des Geschichtenerzählens ein Beispiel, in dem Abhängigkeiten durch den Konstruktor übergeben werden.
Im Codebeispiel ist PhoneInputScreen ein Test-Widget, das auf StatefulWidget basiert und in Scaffold eingeschlossen ist . Es wird in einer Testumgebung mit der Funktion pumpWidget () erstellt :
await tester.pumpWidget(PhoneInputScreen(mock));
Ein echtes Widget kann jedoch die Ausrichtung für verschachtelte Widgets verwenden, für die MediaQuery im Widget-Baum erforderlich ist. Für die Navigation wird wahrscheinlich Navigator.of (Kontext) verwendet. Daher ist es praktischer, das zu testende Widget in MaterialApp oder CupertinoApp zu verpacken:
await tester.pumpWidget( MaterialApp( home: PhoneInputScreen(mock), ), );
Nach dem Erstellen eines Test-Widgets und nach allen damit verbundenen Aktionen müssen Sie tester.pumpAndSettle () aufrufen, damit die Testumgebung alle Änderungen im Status des Widgets verarbeitet.
Integrationstests
Allgemeine Informationen
Im Gegensatz zu Widget-Tests überprüft der Integrationstest die gesamte Anwendung oder einen großen Teil davon. Ziel des Integrationstests ist es, sicherzustellen, dass alle Widgets und Dienste wie erwartet zusammenarbeiten. Der Betrieb des Integrationstests kann im Simulator oder auf dem Gerätebildschirm beobachtet werden. Diese Methode ist ein guter Ersatz für manuelle Tests. Darüber hinaus können Integrationstests verwendet werden, um die Anwendungsleistung zu testen.
Der Integrationstest wird normalerweise auf einem realen Gerät oder Emulator wie iOS Simulator oder Android Emulator durchgeführt.
Dateien mit Integrationstests befinden sich normalerweise im Unterverzeichnis test_driver des Projekts.
Die Anwendung ist vom Testtreibercode isoliert und startet danach. Mit dem Testtreiber können Sie die Anwendung während des Tests steuern. Es sieht so aus:
import 'package:flutter_driver/driver_extension.dart'; import 'package:app_package_name/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); }
Die Tests werden über die Befehlszeile ausgeführt. Wenn der Start der Zielanwendung in der Datei app.dart beschrieben ist und das Testskript app_test.dart heißt, reicht der folgende Befehl aus:
$ flutter drive --target=test_driver/app.dart
Wenn das Testskript einen anderen Namen hat, müssen Sie ihn explizit angeben:
$ flutter drive --target=test_driver/app.dart --driver=test_driver/home_test.dart
Ein Test wird von der Testfunktion erstellt und von der Gruppenfunktion gruppiert.
group('park-flutter app', () {
Dieses Beispiel zeigt den Code zum Erstellen eines Testtreibers, über den Tests mit der zu testenden Anwendung interagieren.
Interaktion mit der getesteten Anwendung
Das FlutterDriver- Tool interagiert mit der Testanwendung über die folgenden Methoden:
- Tippen Sie auf - Senden Sie einen Klick an das Widget.
- waitFor - Warten Sie, bis das Widget auf dem Bildschirm angezeigt wird.
- waitForAbsent - Warten Sie, bis das Widget verschwindet.
- scroll and scrollIntoView , scrollUntilVisible - Scrollen Sie auf dem Bildschirm zum angegebenen Versatz oder zum gewünschten Widget.
- enterText , getText - Geben Sie Text ein oder übernehmen Sie den Text des Widgets.
- Screenshot - Screenshot erstellen;
- requestData - komplexere Interaktion durch einen Funktionsaufruf innerhalb der zu testenden Anwendung.
Es kann vorkommen, dass Sie den globalen Status der Anwendung über den Testcode beeinflussen müssen. Zum Beispiel, um den Integrationstest zu vereinfachen, indem ein Teil der Dienste in der Anwendung durch moki ersetzt wird. In der Anwendung können Sie einen Anforderungshandler angeben, auf den über einen Aufruf von driver.requestData ('some param') im Testcode zugegriffen werden kann:
void main() { Future<String> dataHandler(String msg) async { if (msg == "some param") {
Widget-Suche
Die Suche nach Widgets während des Integrationstests mit dem globalen Suchobjekt unterscheidet sich in der Zusammensetzung der Methoden von ähnlichen Funktionen beim Testen von Widgets. Die allgemeine Bedeutung ändert sich jedoch praktisch nicht:
- im Baum nach Text - find.text , find.widgetWithText ;
- per Schlüssel - find.byValueKey ;
- nach Typ - find.byType ;
- an der Eingabeaufforderung - find.byTooltip ;
- nach semantischem Label - find.bySemanticsLabel ;
- nach Position im Baum find.descendant und find.ancestor .
Fazit
Wir haben nach Möglichkeiten gesucht, das Testen einer mit Flutter geschriebenen Anwendungsschnittstelle zu organisieren. Wir können sowohl Tests implementieren, um zu überprüfen, ob der Code den Anforderungen der technischen Spezifikationen entspricht, als auch Tests mit genau dieser Aufgabe durchführen. Von den festgestellten Mängeln bei Integrationstests gibt es keine Möglichkeit, mit den Systemdialogen der Plattform zu interagieren. Zum Beispiel können Berechtigungsanforderungen vermieden werden, indem Berechtigungen über die Befehlszeile in der Anwendungsinstallationsphase ausgegeben werden, wie in diesem Ticket beschrieben .
Dieser Artikel ist der Ausgangspunkt für die Untersuchung eines Testthemas, das den Leser kurz in die Funktionsweise des Testens von Benutzeroberflächen einführt. Es wird keine Lesedokumentation gespeichert, anhand derer Sie leicht herausfinden können, wie eine bestimmte Klasse oder Methode funktioniert. Schließlich erfordert das Studium eines neuen Themas für sich selbst zunächst ein Verständnis aller laufenden Prozesse als Ganzes ohne übermäßige Details.