Flutter集成测试-简单

我认为许多人已经熟悉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); ... 

手动创建每个DWidgetfind.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 ,然后将其余方法“推迟到以后”。


您可以通过创建将“包装” FutureTestActionTestAction此问题,以便我们可以等到一个操作完成后再进行下一个操作:


 typedef TestAction = Future<void> Function(); 

(本质上, TestAction是返回Future<void>任何函数(或lambda))


现在,您可以轻松运行TestAction序列,而无需等待:


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

TestAction中使用DWidget


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的链接,因此在滚动时,它仅用于指定dyScrolldxScroll


 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), ... ]); 

在测试期间,您可以获取应用程序的ScreenshoterFlutterDriverHelperScreenshoter可将屏幕截图与当前时间一起保存到正确的文件夹中,并可以与TestAction一起TestAction


其他问题及其解决方案


  • 我找不到在时间/日期对话框中单击按钮的标准方法-我必须使用TestHooksTestHooks对于在测试过程中更改当前时间/日期也很有用。
  • DropdownButtonFormField的下拉列表中DropdownButtonFormField您需要为该DropdownMenuItemchild key指定key ,而不是DropdownMenuItemkey ,否则Flutter Driver将找不到它。 另外,在下拉列表中滚动尚不起作用( github.com上的Issue )。
  • FlutterDriver.getCenter方法返回Future<DriverOffset> ,但是DriverOffset不是公共API的一部分( 在github.com上发行
  • 还有一些问题和不明显的东西已经存在。 您可以在一篇精彩的文章中了解它们。 在每次测试开始之前,可以在桌面上运行测试并重置应用程序状态的功能特别有用。
  • 您可以使用Github Actions运行测试。 更多细节在这里

待办


FlutterDriverHelper未来FlutterDriverHelper包括:


  • 如果在访问它时在屏幕上不可见,则会自动滚动到所需的列表项(就像在Android的Kaspresso库中所做的那样 )。 如果可能的话,甚至双向。
  • 使用DwidgetDscrollItem执行的动作的拦截器。

欢迎提出评论和建设性反馈。


更新( TestAction :在版本1.1.0中, TestAction变成了一个具有String name字段的类。 并因此而runTestActions了在runTestActions方法中执行的所有操作的日志记录。

Source: https://habr.com/ru/post/zh-CN483468/


All Articles