Flutter Testing

1,774 阅读18分钟

1.png

前言:

由于当前移动应用的飞速发展,很多公司都需要开发移动应用来获得市场竞争力,在开发移动应用的时候,通常面临对各种框架的选择,尤其是针对 IOS 和 Android 的选择,如 Native 开发:

  • IOS App: Swift/Objective-C
  • Android App: Kotlin/Java

以上两个端的开发都需要开发人员去学习完全不同的技术,同一个 App 在两个端的UI无论是渲染机制还是显示,差异都非常大,开发和测试的effort都会增加,因此,使用超越原生的跨平台技术方案应运而生:使用同一种框架和语言,可以适用于不同的平台。Flutter正是一款非常流行的跨平台开发框架,由于Flutter的实现机制和其他的框架有所不同,因此从测试的角度来说,针对Flutter的测试策略也会有所不同,本章将从Flutter的原理开始,到Flutter测试的测试策略分析到项目实践,详细了解Flutter测试。

1. 什么是Flutter?

Flutter是Google的免费、开源移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter 针对当下以及未来的移动设备进行优化,专注于 Android and iOS 低延迟的输入和高帧率,他主要有以下几个显著的优点:

  • 快速开发:可以实现热加载,代码修改后,应用界面会立即更新,开发和测试都可以通过热加载实现快速debug。
  • 富有表现力和灵活的UI:快速发布聚焦于原生体验的功能。分层的架构允许完全自定义,从而实现难以置信的快速渲染和富有表现力、灵活的设计。
  • 原生性能:Flutter包含了许多核心的widget,如滚动、导航、图标和字体等,这些都可以在iOS和Android上达到原生应用一样的性能。

在全世界,Flutter正在被越来越多的开发者和组织使用,以下是过去五年flutter在市场上的流行趋势图(图片来源:Google Trends):

2.png

可以看出Flutter的使用逐渐开始领先并且保持上升趋势,再看看市场上由Flutter开发的App:

3.jpg

4.jpg

综上所述,Flutter在市场上非常具有竞争力,测试移动端有很大的可能性会接触Flutter,那么当QA接触到Flutter项目的时候,我们可以采用和其他移动端类似的测试策略吗?Flutter和别的项目有什么不同,需要注意什么呢?

2. Flutter的与其他框架的不同

要知道其他移动端测试策略是否适用于Flutter项目,首先要知道Flutter应用与其他应用不同的地方。

2.1 渲染机制的不同:

对移动端的开发,除了上文提到的Native模式以外,主要就是跨平台的框架,其中近几年最流行的是React Native 和 Flutter:

5.png

我们再来看看它们在前端的渲染原理上的一些差别:

6.png

  1. 首先,对于Native的实现(最中间的图,以Android为例),是Android原始的开发框架,由Java调用Skia(2D向量图形处理函数库,目前是Android 官方的图像渲染引擎),再生成CPU/GPU指令,最终渲染在屏幕上。
  2. 对于React Native,通过JS调用Java,再调用Skia,再生成CPU/GPU指令,最终渲染在屏幕上。
  3. 对于Flutter,开发写DART代码(官方唯一指定开发语言)直接调用Skia,再生成CPU/GPU指令,最终渲染在屏幕上。

由以上分析可见:

  • 如果采用纯 Native 实现,那么 Android 和 IOS 的区别非常大,因为它们各自渲染的机制不同,测试人员就需要完全针对两端实现全量测试,耗费的开发和测试的时间和成本都很高。
  • 而React Native,虽然对开发来说,只写了一套代码,但是原则上还是调用了原生的渲染机制,对测试来说,和原生实现的APP测试没有太大的区别。
  • 然而Flutter完全绕过了原生的渲染机制,通过自定义的方式渲染,这样做为Flutter App带来了非常大的优势:
    1. 可以和原生媲美的性能:直接调用底层渲染引擎,没有中间翻译的代价。
    2. 实现各个平台的一致性:绕过原生渲染机制,屏蔽了原生差异。
    3. 高度自定义:拥有大量丰富的自动移组件。

2.2 命名机制和排版不同

Flutter前端元素的属性一般有type, tooltip, keyValue,text等,这些属性中,只有text是可以通过工具 UiAutomator 找到的,但是很多组件根本没有text,如icon,image等,因此,用传统自动化测试工具是不够的。

2.3 Flutter 的测试生态系统不同。

Flutter有完整详细的测试分层:

  • 单元测试:测试单一功能、方法或类。
  • widget 测试:(在其它UI框架称为 组件测试) 测试的单个widget。测试widget涉及多个类,并且需要提供适当的widget生命周期上下文的测试环境。
  • 集成测试: 测试一个完整的应用程序或应用程序的很大一部分。通常,集成测试可以在真实设备或OS仿真器上运行,例如iOS Simulator或Android Emulator。集成测试的目标是验证应用程序作为一个整体正确运行,它所组成的所有widget如预期的那样相互集成。 还可以使用集成测试来验证应用的性能。

以上提到的集成测试就是我们平时所提及的前端e2e test, 可见Flutter已经有非常完备的自动化测试体系,包括对App测试的支持,CI/CD的集成,辅助性能测试等等。

2.4 Build方式不同

Flutter 支持以下三种模式编译 app,也支持使用 headless 模式来测试。

  • Debug模式.
  • Profile模式.
  • Release模式.

这三种模式的区别对测试策略的选择非常重要,因此我们来详细分析一下这三种模式构建的目的和特点。

Debug模式

为了方便dev在开发过程中做调试的,通过运行 flutter run或者通过IDE运行会编译Debug包,Debug模式的包具有以下几个特点:

  1. 热重载功能仅能在Debug模式下运行;
  2. 仿真器和模拟器仅能在Debug模式下运行;
  3. 在Debug模型下,应用的性能可能会掉帧或者卡顿, Profile模式下会更接近真机性能。

Release模式

发布应用的时候,需要选择使用 release 构建模式。通过运行 flutter run --release 就可以编译Release 模式,Release模式的包具有以下几个特点:

  1. 调试信息是不可见。
  2. 调试是禁用的
  3. 编译针对快速启动、快速执行和小的 package 的大小进行了优化。
  4. 只能在真机上运行。
  5. 一般通过CI/CD集成之后打的包就是Release 模式。

Profile 模式

通常用来测试app的性能, profile 模式下,一些调试能力是被保留的—足够分析你的 app 性能。在仿真器和模拟器上,Profile 模式是不可用的,因为他们的行为不能代表真实。

根据这三种编译模式,我们可以分析出,由于release包禁止调试和扩展功能,因此不能在它的基础上跑 e2e test。

假设我们非要运行release 模式来跑 e2e test,运行:

flutter drive --release --target=test_driver/e2e.dart

可以看到以下错误:

Flutter Driver (non-web) does not support running in release mode.
Use --profile mode for testing application performance.
Use --debug (default) mode for testing correctness (with assertions)

2.5 总结 - Flutter测试策略分析

综上所述,由于Flutter的实现原理、前端排版、命名机制、测试生态以及打包编译的特点,对其设计的测试策略会有所不同,主要体现在:

  1. 手动测试
    1. 测试一端即可:屏蔽了原生的差别,各平台的前端呈现几乎一致。
    2. 需要注意个别手机厂家的特殊性:如三星,有自定义的功能,使其某些行为和其他手机上不一致。
    3. 本身不支持proxy抓包:一般移动端测试一定会用到抓包工具,如Charles,但是Flutter本身不允许抓包,因此需要在app实现的时候,通过一定的方式使它能够支持。
    4. 对于Android端,支持ADB(Android Debug Bridge)工具。
  2. 自动化测试
    1. 测试工具采用Flutter官方推荐自动化测试工具:Flutter Driver,具体策略见下文。

    2. 如果使用模拟器跑测试,就需要用Debug模式,也方便集成CI/CD运行 headless 模式跑测试。

    3. 如果想通过 e2e test 测试前端性能,则可采用 profile 模式,但是这种模式下只能在真机上跑,集成CI/CD也需要连接真机。

3. Flutter Driver

3.1 什么是Flutter Driver?

如果熟悉Selenium/WebDriver(web),Espresso(Android)或UI Automation(iOS),那么Flutter Driver就是Flutter与这些集成测试工具的等价物。

Flutter的Driver是:

  • 一个命令行工具 flutter drive
  • 一个包 package:flutter_driver (API)

7.png

即:E2E test script (Made by DART) 调用 Flutter_Driver 提供的API可以对APP内的元素进行定位和定义一系列动作,Flutter_Driver驱动APP在真机或虚拟机上完成自动化测试脚本中的操作。

3.2 Flutter Driver的安装

安装Flutter Driver之前,需要

  • 已经安装配置好flutter环境和依赖,具体可以参考官网教程
  • 已经有项目的代码库。

注:

  1. Flutter 的 E2E test 和其他类型的有所不同,通常E2E test的代码和项目的代码库是分开的,方便QA维护和管理,但是Flutter 的 E2E test需要写在项目的代码库中,具体原因见下文。
  2. 为了演示整个流程,将使用一个demo展示:Counter App。

接下来,准备安装Flutter Driver

3.2.1:添加 Flutter Driver 依赖

假设已经设置好flutter环境和依赖并拥有了项目的代码库,找到  pubspec.yaml 文件**(Flutter 依赖管理tbc)**,添加以下依赖:

dev_dependencies:
  flutter_driver:
    sdk: flutter
  test: any

3.2.2: 创建测试文件

创建一个文件夹,名为 test_driver(Tip:没事别改它的名字,改了跑测试的时候要出问题),然后在这文件夹下创建两个文件:

  1. 第一个文件为只需要以 .dart 结尾就行,如:e2e.dart。该文件的主要目的是引入Flutter Driver 扩展并启动应用程序。
  2. 第二个文件需要在第一个文件的名字后面加个后缀 _test,如:e2e_test.dart。该文件是真正写测试脚本的地方。

加完以后,项目的根目录类似于:

project_name/
  lib/
    main.dart
  test_driver/
    app.dart
    app_test.dart

3.2.3: 创建指令化的Flutter应用程序

一个指令化的应用程序是一个Flutter应用程序,它启用了Flutter Driver 扩展,启用扩展请调用enableFlutterDriverExtension() ,即将下列代码添加到刚才创建的e2e.dart文件里:

import 'package:flutter_driver/driver_extension.dart';
import 'package:project_name/main.dart' as app;

void main() {
  // This line enables the extension.
  enableFlutterDriverExtension();

  // Call the `main()` function of the app, or call `runApp` with
  // any widget you are interested in testing.
  app.main();
}

重要:这个文件首先启用了Flutter Driver 扩展,然后启动了整个应用开发。从第二行可以出来,我们在运行测试的时候,需要启动的应用开发就是调用前端的main.dart文件,这就是我们为什么需要把测试脚本和项目集成在一个代码库的原因。

3.2.4: 编写 E2E Health Check 测试

e2e_test.dart 文件中添加以下文件,每个步骤在文件中有注释:

// Imports the Flutter Driver API.
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App Test', () {
    FlutterDriver driver;

    // Connect to the Flutter driver before running any tests.
    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    // Close the connection to the driver after the tests have completed.
    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });
  });
}

上面的脚本在测试开始之前与 Flutter Driver 建立连接,在开始测试之后关闭连接。在开始编写真正的测试脚本之前,需要通过 checkHealth() 测试与Flutter Driver的连接是否建立成功:

test('check flutter driver health', () async {
  Health health = await driver.checkHealth();
  print(health.status);
});

3.2.5: 运行测试

完成以上步骤,连上虚拟机或者真机,运行以下命令:

flutter drive --target=test_driver/e2e.dart

该命令将:

  • 构建 -target 应用,并将其安装在设备上。
  • 启动应用。
  • 运行test_driver/下的e2e.dart

flutter drive 命令使用一种约定来查找与--target应用程序在同一目录中具有相同文件名但是具有_test后缀的测试文件。

如出现以下返回,则 Flutter Driver 已经配置好了:

00:02 +0: Counter App check flutter driver health

HealthStatus.ok

00:02 +1: Counter App (tearDownAll)

00:02 +1: All tests passed!

Stopping application instance.

您可能注意到,每次 run 测试脚本,都需要启动应用,再跑测试,耗费的时候很长,flutter driver 提供了 hot reload 功能,但是只能适用于 local 环境,详细请参考:medium.com/flutter-com…

3.3 编写E2E项目测试脚本实例

3.3.1: 编写测试脚本

接下来需要写真正测试脚本,我们要测试的demo为一个Counter,如下图所示:

counter.png

测试流程:

  1. 找到增加计数的button (The yellow button)
  2. 点击它
  3. 验证计数从0变成1
  4. 再次点击
  5. 验证计数从0变成2

首先需要找到这个button,找元素可以使用以下几种方法:

byTooltip(...)
byType(...)
byText(...)
byValueKey(...)
bySemanticsLabel(...)

在1.3章中提到flutter命名机制,组件的属性即type, tooltip, keyValue,text等,因此我们需要根据改组件有什么属性,再根据对应的方法去找到它,如对于这个button,它的实现为:

floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      )

它有属性tooltip都为"Increment",因此可以通过以下方式找到它:

final buttonFinder = find.byTooltip('Increment');

找到该组件,需要点击,可以用以下方法:

await driver.tap(buttonFinder);

最后验证计数是否为1,需要先找到计数组件,它的实现为:

Text(
  '$_counter',
  style: Theme.of(context).textTheme.display4,
  key: ValueKey("counterText"),
),

可见它的text属性的内容是不固定的,会随着计数而改变,因此使用keyValue

final counterTextFinder = find.byValueKey('counterText');

最后验证结果,验证结果可以使用以下几种方式:

//Get text from counterTextFinder, then compare result
expect(await driver.getText(counterTextFinder), "1");

//wait this text 
await driver.waitFor(find.text('1'));

重复一次,整个完整的测试为:

test('Increment the counter', () async {
  // First, find widget
  final counterTextFinder = find.byValueKey('counterText');
  final buttonFinder = find.byTooltip('Increment');
  
  // Then, tap on the button
  await driver.tap(buttonFinder);

  // Then, verify the counter text has been incremented by 1
  expect(await driver.getText(counterTextFinder), "1");

  // First, tap on the button
  await driver.tap(buttonFinder);

  // Then, verify the counter text has been incremented by 1
  expect(await driver.getText(counterTextFinder), "2");
});

除了以上用到的方法,常用的还有:

//enter text
final example = find.byValueKey('example');
await driver.tap(example);
await driver.enterText("text");

//scroll screen until the widget is completely visible 
final example = find.text('example');
await driver.scrollIntoView(example);

//to wait until the target specified in finder is visible
final example = find.text('example');
await driver.waitFor(example);

//to wait until the target specified in finder is no longer available
final example = find.text('example');
await driver.waitForAbsent(example);

更多的方法可以参考 flutter driver API 文档。

以上是关于功能测试的测试方法,关于visual testing,flutter driver仍然提供了非常棒的截图功能:

e2e_test.dart文件中的setUpAll()方法中,添加以下代码:

new Directory(‘screenshots’).create();
await Directory('e2e_test/screenshots').create();

在同一个文件中添加takeScreenshot()方法:

Future<void> takeScreenshot(FlutterDriver driver, String path) async {
  var pixels = await driver.screenshot();
  var file = File('e2e_test/screenshots/' + path);
  await file.writeAsBytes(pixels);
}

在测试用例中添加takeScreenshot()命令:

await takeScreenshot(driver, 'screenshot_name.png');

运行 flutter drive --target=test/e2e.dart 可以出现以下结果:


00:01 +0: Counter App check flutter driver health
HealthStatus.ok
00:01 +1: Counter App Increment the counter
00:04 +2: Counter App Test with alert window
00:05 +3: Counter App (tearDownAll)
00:05 +3: All tests passed!
Stopping application instance.

查看项目目录 e2e_test/screenshots会出现截图:

截图.png

3.3.2: 查找定位widget

做Native开发的移动端自动化,可以用 Android Sdk 包中的 uiautomateviewer 和 Appium Desktop 中的 Appium Inspector,但是flutter app不能使用这些工具,原因是上文提到的UI命名机制和前端布局的差别,因此,我们需要用到flutter插件Flutter Inspector 和 Dart Devtools。

1. Flutter Inspector

首先打开 IntellJ,运行 flutter run, 可以看到面板上有 Flutter Inspector。

找定位.jpg

点击 Flutter Inspector 会看到如下界面:

找定位2,点击看到如下界面.jpg

通过 Flutter Inspector,我们就可以看到应用程序的 Widget 和 Render Tree, 点击下图所示按钮激活Select Widget Mode:

Select Widget Mode.jpg

然后点击模拟器或者真机上想要定位的widget,则:

  • Flutter Inspector 会自动显示该 widget 的结构和 render details。
  • 会自动显示该widget在源程序中定义的属性。

源程序中定义的属性.jpg

这样就可以找到该元素的属性啦。

2. Dart DevTools

我们还可以通过另一种方式定位widget,打开 IntellJ,运行 flutter run, 点击下图所示 DevTools 按钮:

图所示 DevTools 按钮:.jpg

等待几秒钟,会自动开启一个web网页: 会自动开启一个web网页:.png

点击下图所示按钮 Select Widget Mode(标记为1),点击模拟器或真机上想要定位的元素,Dart DevTools 会自动显示该元素所在的结构和layout等信息(该流程和使用Flutter Inspector类似)

ayout等信息(该流程和使用Flutter Inspector类似).jpg 通过上图可见,其实通过 Dart DevTools 查看元素属性其实就是使用 Flutter Inspector,因此,如果只是查看元素的话,只用第一种方式就足够了。

再看 Dart DevTools 的 web 面板的导航,除了 Flutter Inspector 以外,还有 “Performance,CPU profile”等与性能相关的属性,因此,我们可以通过 Dart DevTools 来辅助测试应用程序的性能。

3.3.3: Flutter Driver 扩展

  1. BDD (Behavior-Driven Development) 工具: flutter_gherkin

BDD是用自然语言或类自然语言,按照编写用户故事或者用户用例的方式,以功能使用者的视角,描述并编写测试用例,BDD最显著的优点在于:

  • 加强团队内部之间的合作和跨功能团队的工作交流。
  • 确保用户故事从业务角度出发,技术角度实现,最终从业务角度测试的完整流程。
  • 确保自动化测试脚本容易编写和读懂。

flutter driver也提供了支持BDD的方法:flutter_gherkin。然而,flutter 对 BDD 的支持并没有那么完善,相反,它由非常多的限制和规则:

  1. 不支持映射:即在 testCase.feature里的自然语言不能通过映射直接找到与其对应的step
  2. 每个feature里面的每个step的class file都需要分开:也就是如果有十个不同的步骤,就要建十个不同的Dart file。
  3. 把需要执行的步骤全部定义在一个config文件中,真正执行测试步骤的是这个config里面的步骤。

如果一个项目的自动化测试用例非常多,那么使用 flutter_gherkin 将会非常难以维护,编写也不易,所以并不推荐在flutter的测试中实践BDD。

  1. Visual Testing 工具 :Micoo.

对 E2E test 来说,采用 visual testing 的策略越来越常见,而Micoo 正是用于 visual regression test 的基于像素的屏幕快照比较工具,它是一个Web应用程序,通过设置基准图片,可以将最新的截图与之对比,输出测试结果。flutter driver 结合 Micoo 的好处有:

  1. 通过 Micoo 可以轻松比较不同版本的截图,灵活设置baseline。
  2. Micoo 的配置、图片上传流程简单易懂。
  3. 可以将同时实现功能测试和视觉测试,如:
    1. 通过代码断言实现功能测试。
    2. 通过图片对比实现视觉测试。

4. Flutter Driver 项目实践:

测试环境:

为了测试真实的e2e, 测试的环境选择的是: QA 环境(项目上也有API 自动化测试)。

测试类型:

为了同时实现功能测试和视觉测试,采取了两个自动化测试帐号:

  1. 账号1不会做任何操作引起用户数据的变化,最大程度保证每次的截图是一致的,然后将账号1产生的截图上传 Micoo,自动进行图片对比。
  2. 账号2会实现各种操作,通过代码断言判断测试结果。

测试机

为了在pipeline上运行自动化测试,采用模拟器运行程序。

CI/CD 运行策略

**工具:**Codemagic,和前端支持部署工具一致。

自动化测试流程

  1. 由于只有debug包能跑自动化测试,因此,必须单独搭建一条pipeline部署debug版本的包。
  2. 启动虚拟机,运行 e2e test。
  3. Pipeline输出功能测试的结果和视觉测试的截图,并自动将截图上传到Micoo服务器。
  4. Micoo 自动对比图片,输出结果。

5. 总结:

总体来说, flutter driver 与 Flutter 项目相互支持得非常好,习惯了写法以后,操作起来还是比较快速的,但是由于目前并不是十分成熟,功能有限,然而对于项目上的大多数场景,完全是足够的。采用功能测试和视觉测试的搭配,不仅能真实的测试前后端,还能捕捉到容易被忽视的细节和改动,是一个非常好的实践,如果读者有什么建议和意见,欢迎沟通。