当需要同时为多个平台快速创建美观而响应迅速的应用程序时,人们会记得Flutter,但是如何保证“快速”代码的质量呢?
您会感到惊讶,但是Flutter不仅可以确保代码质量,而且可以保证可视界面的可操作性。
在本文中,我们将检查Flutter上的测试情况,我们将分析小部件测试和整个应用程序的集成测试。

一年多以前,我开始研究Flutter,在正式发布之前,在研究过程中找到任何开发信息都没有问题。 当我想尝试TDD时 ,事实证明,有关测试的信息非常少。 俄语,通常几乎没有。 根据Flutter测试的源代码和英文罕见文章,必须独立研究测试问题。 我在一篇文章中描述了我在测试视觉元素方面研究的所有内容,以帮助那些刚刚开始研究该主题的人。
一般资讯
小部件测试将测试单个小部件。 也可以称为组件测试。 该测试的目的是证明小部件的用户界面按计划进行外观和交互。 测试窗口小部件需要一个测试环境,该环境应为窗口小部件的生命周期提供适当的上下文。
经过测试的窗口小部件可以接收用户操作和事件,并对它们进行响应,并构建子窗口小部件树。 因此,小部件测试比单元测试更为复杂。 但是,就像单元测试一样,小部件测试环境是一个简单的模拟,比功能完善的用户界面系统要简单得多。
窗口小部件测试允许您隔离和测试可视界面的单个元素的行为。 而且,值得注意的是,可以在控制台中执行所有检查,这对于作为CI / CD流程一部分运行的测试是理想的。
包含测试的文件通常位于项目的test子目录中。
可以使用以下命令从IDE或控制台运行测试:
$ flutter test
在这种情况下,将执行test子目录中所有带有* _test.dart掩码的测试 。
您可以通过指定文件名来运行单独的测试:
$ flutter test test/phone_screen_test.dart
该测试由testWidgets函数创建,该函数接收一个工具作为tester参数,测试代码可通过该工具与被测试的小部件进行交互:
testWidgets(' ', (WidgetTester tester) async {
要将测试合并到逻辑块中,可以在组函数内部将测试函数合并为组:
group(' ', (){ testWidgets(' ', (WidgetTester tester) async {
使用setUp和tearDown函数可以在每次测试之前和之后执行一些代码。 因此,使用setUpAll和tearDownAll函数可以在所有测试之前和之后运行代码,如果在组内调用这些函数,则在执行该组中的所有测试之前,它们将被称为“之前”和“之后”:
setUp(() {
小部件搜索
为了对嵌套窗口小部件执行某些操作,您需要在窗口小部件树中找到它。 为此,有一个全局查找对象,可用于查找小部件:
- 在树中通过文本-find.text , find.widgetWithText ;
- 通过键-find.byKey ;
- 通过图标-find.byIcon , find.widgetWithIcon ;
- 按类型-find.byType ;
- 根据树中的位置-find.descendant和find.ancestor ;
- 使用分析列表上的小部件的功能-find.byWidgetPredicate 。
测试小部件交互
WidgetTester类提供用于创建测试窗口小部件,等待其状态更改以及对这些窗口小部件执行某些操作的功能。
小部件中的任何更改都会导致其状态发生更改。 但是测试环境不会同时重建小部件。 您必须通过调用pump或pumpAndSettle函数独立地向测试环境指示要重建小部件。
- pumpWidget-创建一个测试小部件;
- pump-开始处理小部件的状态转换,并等待其在指定的超时(默认为100毫秒)内完成;
- pumpAndSettle-循环调用Pump以在给定的超时时间内更改状态(默认为100毫秒),这是等待所有动画完成的时间;
- 点击 -向小部件发送点击;
- longPress-长按;
- 猛击-滑动/滑动
- 拖动 -转移;
- enterText-文本输入。
测试可以实现肯定的方案,检查计划的机会,也可以实现否定的方案,以确保它们不会导致致命的后果,例如,当用户单击错误的方向并且未输入所需内容时:
await tester.enterText(find.byKey(Key('phoneField')), 'bla-bla-bla');
在对窗口小部件进行任何操作之后,您需要调用tester.pumpAndSettle()来更改状态。
茂喜
许多人都熟悉Mockito库。 事实证明,这个来自Java世界的库非常成功,以至于有许多编程语言(包括Dart)都实现了该库。
要进行连接,必须将依赖项添加到项目中。 将以下行添加到pubspec.yaml文件:
dependencies: mockito: any
并连接测试文件:
import 'package:mockito/mockito.dart';
该库使您可以创建受测试的小部件所依赖的Moque类,从而使测试更简单,并且仅涵盖我们正在测试的代码。
例如,如果我们测试PhoneInputScreen小部件,该小部件在单击时使用AuthInteractor服务向authInteractor.checkAccess ()后端执行请求,然后用模拟代替服务,我们可以检查最重要的事情-访问此服务的事实。
依赖小怪被创建为Mock类的后代,并实现了依赖接口:
class AuthInteractorMock extends Mock implements AuthInteractor {}
Dart中的类也是接口,因此不需要像某些其他编程语言一样分别声明接口。
要确定mok的功能,可以使用when函数,它使您可以确定mok对调用特定函数的响应:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.value(true));
Moki可以返回错误或错误数据:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.error(UnknownHttpStatusCode(null)));
支票
在测试期间,您可以检查屏幕上的小部件。 这使您可以根据所需小部件的可见性来确保屏幕的新状态正确:
expect(find.text(' '), findsOneWidget); expect(find.text(' '), findsNothing);
测试完成后,您还可以检查在测试期间调用了mob类的哪些方法,以及调用了多少次。 例如,这对于了解是否经常请求此数据或该数据,应用程序状态是否有不必要的更改是必要的:
verify(appComponent.authInteractor).called(1); verify(authInteractor.checkAccess(any)).called(1); verifyNever(appComponent.profileInteractor);
侦错
测试在控制台中执行,没有任何图形。 您可以在调试模式下运行测试,并在小部件代码中设置断点。
要了解小部件树中发生的事情,可以使用debugDumpApp()函数,该函数在测试代码中调用时,将在给定时间在控制台中显示整个小部件树的层次结构的文本表示形式。
要了解小部件如何使用moki,有一个logInvocations()函数。 它以艾滋列表为参数,并向控制台发出在测试中执行的这些艾滋的方法调用序列。
这种结论的一个例子如下。 VERIFIED标记位于使用verify函数在测试中检查的呼叫上:
AppComponentMock.sessionChangedInteractor [VERIFIED] AppComponentMock.authInteractor [VERIFIED] AuthInteractorMock.checkAccess(71111111111)
准备工作
所有依赖项都应以mok的形式提交给经过测试的窗口小部件:
class SomeComponentMock extends Mock implements SomeComponent {} class AuthInteractorMock extends Mock implements AuthInteractor {}
依赖关系到测试组件的转移应以您的应用程序接受的某种方式执行。 为了简化讲故事,考虑一个通过构造函数传递依赖项的示例。
在代码示例中, PhoneInputScreen是一个基于包装在Scaffold中的 StatefulWidget的测试窗口小部件。 它是在测试环境中使用pumpWidget()函数创建的:
await tester.pumpWidget(PhoneInputScreen(mock));
但是,真正的窗口小部件可以对嵌套窗口小部件使用对齐方式,这需要在窗口小部件树中使用MediaQuery ,它可能会获得Navigator.of(上下文)进行导航,因此将要测试的窗口小部件包装在MaterialApp或CupertinoApp中更为实用:
await tester.pumpWidget( MaterialApp( home: PhoneInputScreen(mock), ), );
创建测试窗口小部件并对其执行任何操作后,您需要调用tester.pumpAndSettle(),以便测试环境处理窗口小部件状态中的所有更改。
整合测试
一般资讯
与窗口小部件测试不同,集成测试将检查整个应用程序或其中的大部分。 集成测试的目标是确保所有小部件和服务都能按预期工作。 可以在模拟器或设备屏幕上观察集成测试的操作。 此方法可以很好地替代手动测试。 此外,集成测试可用于测试应用程序性能。
集成测试通常在真实的设备或仿真器上执行,例如iOS Simulator或Android Emulator。
包含集成测试的文件通常位于项目的test_driver子目录中。
该应用程序与测试驱动程序代码隔离,并在之后启动。 测试驱动程序允许您在测试过程中控制应用程序。 看起来像这样:
import 'package:flutter_driver/driver_extension.dart'; import 'package:app_package_name/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); }
这些测试是从命令行运行的。 如果在app.dart文件中描述了目标应用程序的启动,并且测试脚本名为app_test.dart ,那么以下命令就足够了:
$ flutter drive --target=test_driver/app.dart
如果测试脚本的名称不同,则需要明确指定它:
$ flutter drive --target=test_driver/app.dart --driver=test_driver/home_test.dart
测试由测试功能创建,并按组功能分组。
group('park-flutter app', () {
此示例显示了用于创建测试驱动程序的代码,通过该驱动程序,测试可以与被测应用程序交互。
与测试应用程序的交互
FlutterDriver工具通过以下方法与测试应用程序进行交互:
- 点击 -向小部件发送点击;
- waitFor-等待窗口小部件出现在屏幕上;
- waitForAbsent-等待小部件消失;
- 滚动和scrollIntoView , scrollUntilVisible-将屏幕滚动到指定的偏移量或所需的小部件;
- enterText , getText-输入文本或获取小部件的文本;
- 屏幕截图 -获取屏幕截图;
- requestData-通过被测应用程序内部的函数调用进行更复杂的交互。
在某些情况下,您需要从测试代码影响应用程序的全局状态。 例如,通过使用moki替换应用程序中的部分服务来简化集成测试。 在应用程序中,您可以指定一个请求处理程序,可以通过调用测试代码中的driver.requestData(“某些参数”)来访问该请求处理程序:
void main() { Future<String> dataHandler(String msg) async { if (msg == "some param") {
小部件搜索
在与全局查找对象进行集成测试期间,搜索窗口小部件的方法组成与测试窗口小部件中的类似功能有所不同。 但是,一般含义实际上并没有改变:
- 在树中通过文本-find.text , find.widgetWithText ;
- 通过键-find.byValueKey ;
- 按类型-find.byType ;
- 在提示符下-find.byTooltip ;
- 通过语义标签-find.bySemanticsLabel ;
- 按在树中的位置find.descendant和find.ancestor 。
结论
我们研究了组织测试使用Flutter编写的应用程序接口的方法。 我们既可以执行测试以验证代码是否符合技术规范的要求,又可以执行此任务进行测试。 在集成测试的已指出的缺点中-无法与平台的系统对话框进行交互。 但是,例如,可以通过在应用程序安装阶段从命令行发布权限来避免权限请求, 如此票证中所述 。
本文是探讨测试主题的起点,该主题向读者简要介绍了用户界面测试的工作方式。 它不会保存阅读文档,从中可以很容易地找到特定类或方法的功能。 毕竟,为自己研究一个新主题首先需要对所有正在进行的过程有一个整体的了解,而又没有过多的细节。