我认为许多人已经熟悉Flutter,并且至少出于兴趣的考虑,他们开始在Flutter上进行简单的应用程序。 现在是确保一切都能按需工作的时候了,集成测试将帮助我们实现这一目标。

Flutter上的集成测试是使用Flutter驱动程序编写的,其官方网站上提供了一个简单易懂的教程 。 这种测试的结构类似于Android世界中的Espresso 。 首先,您需要在屏幕上找到UI元素:
final SerializableFinder button = find.byValueKey("button");
然后对他们执行一些操作:
driver = await FlutterDriver.connect(); ... await driver.tap(button);
并验证所需的UI元素是否处于所需状态:
final SerializableFinder text = find.byValueKey("text"); expect(await driver.getText(text), "some text");
当然,通过一个简单的示例,一切看起来都很基本。 但是随着被测应用程序的增长和测试数量的增加,我不想在每次测试之前重复搜索UI元素。 另外,由于可能有许多屏幕,因此您将需要构造这些UI元素。 为此,使编写测试更加方便。
屏幕对象
在Android( Kakao )中,通过将每个屏幕中的UI元素分组到Screen(页面对象)中来解决此问题。 除了在Flutter中使用UI元素执行操作外,您还可以在此处应用类似的方法,不仅需要Finder
(搜索UI元素),还需要FlutterDriver
(执行操作),因此需要在其中存储指向FlutterDriver
的链接Screen
。
要定义每个UI元素,我们添加DWidget
类(在这种情况下,D是单词Dart)。 要创建DWidget
将需要FlutterDriver
和一个FlutterDriver
,它将与该UI元素一起执行操作,该ValueKey
与我们要与之交互的应用程序中的小部件的ValueKey
Flutter重合:
class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; DWidget(this._driver, dynamic valueKey) : _finder = find.byValueKey(valueKey); ...
手动创建每个DWidget
时find.byValueKey(…)
DWidget
方便,因此最好将ValueKey
值传递给ValueKey
,并且DWidget
本身将获得所需的SerializableFinder
。 创建每个FlutterDriver
时手动传递FlutterDriver
也不太方便,因此您可以将FlutterDriver
存储在BaseScreen
并将其传输到DWidget
,并为BaseScreen
添加新方法来创建BaseScreen
:
abstract class BaseScreen { final FlutterDriver _driver; BaseScreen(this._driver); DWidget dWidget(dynamic key) => DWidget(_driver, key); ...
因此,创建Screens类并在其中获取UI元素将更加容易:
class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get button => dWidget('button'); DWidget get textField => dWidget('text_field'); ... }
摆脱await
用FlutterDriver
编写测试时,另一件不太方便的事情是需要在每个操作之前添加await
:
await driver.tap(button); await driver.scrollUntilVisible(list, checkBox); await driver.tap(checkBox); await driver.tap(text); await driver.enterText("some text");
忘记await
很容易,没有它,测试将无法正常进行,因为driver
方法返回Future<void>
并且在不await
情况下调用await
将执行await
,直到方法内的第一次await
,然后将其余方法“推迟到以后”。
您可以通过创建将“包装” Future
的TestAction
来TestAction
此问题,以便我们可以等到一个操作完成后再进行下一个操作:
typedef TestAction = Future<void> Function();
(本质上, TestAction
是返回Future<void>
任何函数(或lambda))
现在,您可以轻松运行TestAction
序列,而无需等待:
Future<void> runTestActions(Iterable<TestAction> actions) async { for (final action in actions) { await action(); } }
DWidget
用于与UI元素进行交互,如果这些动作是TestAction
,则将非常方便,以便可以在runTestAction
方法中使用它们。 为此, DWidget
类将具有操作方法:
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); }; ... }
现在您可以编写测试,如下所示:
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"), ]);
如果您需要在runTestActions
中执行与DWidget
不相关的DWidget
,则只需创建一个返回Future<void>
的lambda即可:
await runTestActions([ mainScreen.result.hasText("summa = 0"), () => driver.requestData("some_message"), () async => print("some_text"), mainScreen.field_1.setText("3"), ]);
FlutterDriverHelper
FlutterDriver
有几种与UI元素进行交互的方法(按,接收和输入文本,滚动等),对于这些方法, DWidget
具有返回TestAction
相应方法。
为了方便起见,本文中描述的所有代码都在FlutterDriverHelper
上作为FlutterDriverHelper
库发布 。
对于滚动列表,在其中动态创建元素的列表(例如ListView.builder
), FlutterDriver
有一个scrollUntilVisible
方法:
Future<void> scrollUntilVisible( SerializableFinder scrollable, SerializableFinder item, { double alignment = 0.0, double dxScroll = 0.0, double dyScroll = 0.0, Duration timeout, }) async { ... }
此方法沿指定方向滚动可滚动窗口小部件,直到item
窗口小部件出现在屏幕上(或直到timeout
)。 为了不使每次滚动都滚动,添加了DScrollItem类,该类继承了DWidget
并表示一个列表项。 它包含一个指向scrollable
的链接,因此在滚动时,它仅用于指定dyScroll
或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), ... ]);
在测试期间,您可以获取应用程序的Screenshoter
, FlutterDriverHelper
的Screenshoter
可将屏幕截图与当前时间一起保存到正确的文件夹中,并可以与TestAction
一起TestAction
。
其他问题及其解决方案
- 我找不到在时间/日期对话框中单击按钮的标准方法-我必须使用
TestHooks
。 TestHooks
对于在测试过程中更改当前时间/日期也很有用。 - 在
DropdownButtonFormField
的下拉列表中DropdownButtonFormField
您需要为该DropdownMenuItem
的child
key
指定key
,而不是DropdownMenuItem
的key
,否则Flutter Driver
将找不到它。 另外,在下拉列表中滚动尚不起作用( github.com上的Issue )。 FlutterDriver.getCenter
方法返回Future<DriverOffset>
,但是DriverOffset
不是公共API的一部分( 在github.com上发行 )- 还有一些问题和不明显的东西已经存在。 您可以在一篇精彩的文章中了解它们。 在每次测试开始之前,可以在桌面上运行测试并重置应用程序状态的功能特别有用。
- 您可以使用Github Actions运行测试。 更多细节在这里 。
待办
FlutterDriverHelper
未来FlutterDriverHelper
包括:
- 如果在访问它时在屏幕上不可见,则会自动滚动到所需的列表项(就像在Android的Kaspresso库中所做的那样 )。 如果可能的话,甚至双向。
- 使用
Dwidget
或DscrollItem
执行的动作的拦截器。
欢迎提出评论和建设性反馈。
更新( TestAction
) :在版本1.1.0中, TestAction
变成了一个具有String name
字段的类。 并因此而runTestActions
了在runTestActions
方法中执行的所有操作的日志记录。