前言:
由于当前移动应用的飞速发展,很多公司都需要开发移动应用来获得市场竞争力,在开发移动应用的时候,通常面临对各种框架的选择,尤其是针对 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):
可以看出Flutter的使用逐渐开始领先并且保持上升趋势,再看看市场上由Flutter开发的App:
综上所述,Flutter在市场上非常具有竞争力,测试移动端有很大的可能性会接触Flutter,那么当QA接触到Flutter项目的时候,我们可以采用和其他移动端类似的测试策略吗?Flutter和别的项目有什么不同,需要注意什么呢?
2. Flutter的与其他框架的不同
要知道其他移动端测试策略是否适用于Flutter项目,首先要知道Flutter应用与其他应用不同的地方。
2.1 渲染机制的不同:
对移动端的开发,除了上文提到的Native模式以外,主要就是跨平台的框架,其中近几年最流行的是React Native 和 Flutter:
我们再来看看它们在前端的渲染原理上的一些差别:
- 首先,对于Native的实现(最中间的图,以Android为例),是Android原始的开发框架,由Java调用Skia(2D向量图形处理函数库,目前是Android 官方的图像渲染引擎),再生成CPU/GPU指令,最终渲染在屏幕上。
- 对于React Native,通过JS调用Java,再调用Skia,再生成CPU/GPU指令,最终渲染在屏幕上。
- 对于Flutter,开发写DART代码(官方唯一指定开发语言)直接调用Skia,再生成CPU/GPU指令,最终渲染在屏幕上。
由以上分析可见:
- 如果采用纯 Native 实现,那么 Android 和 IOS 的区别非常大,因为它们各自渲染的机制不同,测试人员就需要完全针对两端实现全量测试,耗费的开发和测试的时间和成本都很高。
- 而React Native,虽然对开发来说,只写了一套代码,但是原则上还是调用了原生的渲染机制,对测试来说,和原生实现的APP测试没有太大的区别。
- 然而Flutter完全绕过了原生的渲染机制,通过自定义的方式渲染,这样做为Flutter App带来了非常大的优势:
- 可以和原生媲美的性能:直接调用底层渲染引擎,没有中间翻译的代价。
- 实现各个平台的一致性:绕过原生渲染机制,屏蔽了原生差异。
- 高度自定义:拥有大量丰富的自动移组件。
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模式的包具有以下几个特点:
- 热重载功能仅能在Debug模式下运行;
- 仿真器和模拟器仅能在Debug模式下运行;
- 在Debug模型下,应用的性能可能会掉帧或者卡顿, Profile模式下会更接近真机性能。
Release模式
发布应用的时候,需要选择使用 release 构建模式。通过运行 flutter run --release
就可以编译Release 模式,Release模式的包具有以下几个特点:
- 调试信息是不可见。
- 调试是禁用的。
- 编译针对快速启动、快速执行和小的 package 的大小进行了优化。
- 只能在真机上运行。
- 一般通过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的实现原理、前端排版、命名机制、测试生态以及打包编译的特点,对其设计的测试策略会有所不同,主要体现在:
- 手动测试:
- 测试一端即可:屏蔽了原生的差别,各平台的前端呈现几乎一致。
- 需要注意个别手机厂家的特殊性:如三星,有自定义的功能,使其某些行为和其他手机上不一致。
- 本身不支持proxy抓包:一般移动端测试一定会用到抓包工具,如Charles,但是Flutter本身不允许抓包,因此需要在app实现的时候,通过一定的方式使它能够支持。
- 对于Android端,支持ADB(Android Debug Bridge)工具。
- 自动化测试:
-
测试工具采用Flutter官方推荐自动化测试工具:Flutter Driver,具体策略见下文。
-
如果使用模拟器跑测试,就需要用Debug模式,也方便集成CI/CD运行 headless 模式跑测试。
-
如果想通过 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)
即:E2E test script (Made by DART) 调用 Flutter_Driver 提供的API可以对APP内的元素进行定位和定义一系列动作,Flutter_Driver驱动APP在真机或虚拟机上完成自动化测试脚本中的操作。
3.2 Flutter Driver的安装
安装Flutter Driver之前,需要
- 已经安装配置好flutter环境和依赖,具体可以参考官网教程。
- 已经有项目的代码库。
注:
- Flutter 的 E2E test 和其他类型的有所不同,通常E2E test的代码和项目的代码库是分开的,方便QA维护和管理,但是Flutter 的 E2E test需要写在项目的代码库中,具体原因见下文。
- 为了演示整个流程,将使用一个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:没事别改它的名字,改了跑测试的时候要出问题),然后在这文件夹下创建两个文件:
- 第一个文件为只需要以
.dart
结尾就行,如:e2e.dart
。该文件的主要目的是引入Flutter Driver 扩展并启动应用程序。 - 第二个文件需要在第一个文件的名字后面加个后缀
_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,如下图所示:
测试流程:
- 找到增加计数的button (The yellow button)
- 点击它
- 验证计数从0变成1
- 再次点击
- 验证计数从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
会出现截图:
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。
点击 Flutter Inspector 会看到如下界面:
通过 Flutter Inspector,我们就可以看到应用程序的 Widget 和 Render Tree, 点击下图所示按钮激活Select Widget Mode:
然后点击模拟器或者真机上想要定位的widget,则:
- Flutter Inspector 会自动显示该 widget 的结构和 render details。
- 会自动显示该widget在源程序中定义的属性。
这样就可以找到该元素的属性啦。
2. Dart DevTools
我们还可以通过另一种方式定位widget,打开 IntellJ,运行 flutter run, 点击下图所示 DevTools 按钮:
等待几秒钟,会自动开启一个web网页:
点击下图所示按钮 Select Widget Mode(标记为1),点击模拟器或真机上想要定位的元素,Dart DevTools 会自动显示该元素所在的结构和layout等信息(该流程和使用Flutter Inspector类似)
通过上图可见,其实通过 Dart DevTools 查看元素属性其实就是使用 Flutter Inspector,因此,如果只是查看元素的话,只用第一种方式就足够了。
再看 Dart DevTools 的 web 面板的导航,除了 Flutter Inspector 以外,还有 “Performance,CPU profile”等与性能相关的属性,因此,我们可以通过 Dart DevTools 来辅助测试应用程序的性能。
3.3.3: Flutter Driver 扩展
- BDD (Behavior-Driven Development) 工具: flutter_gherkin
BDD是用自然语言或类自然语言,按照编写用户故事或者用户用例的方式,以功能使用者的视角,描述并编写测试用例,BDD最显著的优点在于:
- 加强团队内部之间的合作和跨功能团队的工作交流。
- 确保用户故事从业务角度出发,技术角度实现,最终从业务角度测试的完整流程。
- 确保自动化测试脚本容易编写和读懂。
flutter driver也提供了支持BDD的方法:flutter_gherkin。然而,flutter 对 BDD 的支持并没有那么完善,相反,它由非常多的限制和规则:
- 不支持映射:即在
testCase.feature
里的自然语言不能通过映射直接找到与其对应的step
。 - 每个feature里面的每个step的class file都需要分开:也就是如果有十个不同的步骤,就要建十个不同的Dart file。
- 把需要执行的步骤全部定义在一个config文件中,真正执行测试步骤的是这个config里面的步骤。
如果一个项目的自动化测试用例非常多,那么使用 flutter_gherkin 将会非常难以维护,编写也不易,所以并不推荐在flutter的测试中实践BDD。
- Visual Testing 工具 :Micoo.
对 E2E test 来说,采用 visual testing 的策略越来越常见,而Micoo 正是用于 visual regression test 的基于像素的屏幕快照比较工具,它是一个Web应用程序,通过设置基准图片,可以将最新的截图与之对比,输出测试结果。flutter driver 结合 Micoo 的好处有:
- 通过 Micoo 可以轻松比较不同版本的截图,灵活设置baseline。
- Micoo 的配置、图片上传流程简单易懂。
- 可以将同时实现功能测试和视觉测试,如:
- 通过代码断言实现功能测试。
- 通过图片对比实现视觉测试。
4. Flutter Driver 项目实践:
测试环境:
为了测试真实的e2e, 测试的环境选择的是: QA 环境(项目上也有API 自动化测试)。
测试类型:
为了同时实现功能测试和视觉测试,采取了两个自动化测试帐号:
- 账号1不会做任何操作引起用户数据的变化,最大程度保证每次的截图是一致的,然后将账号1产生的截图上传 Micoo,自动进行图片对比。
- 账号2会实现各种操作,通过代码断言判断测试结果。
测试机
为了在pipeline上运行自动化测试,采用模拟器运行程序。
CI/CD 运行策略
**工具:**Codemagic,和前端支持部署工具一致。
自动化测试流程:
- 由于只有debug包能跑自动化测试,因此,必须单独搭建一条pipeline部署debug版本的包。
- 启动虚拟机,运行 e2e test。
- Pipeline输出功能测试的结果和视觉测试的截图,并自动将截图上传到Micoo服务器。
- Micoo 自动对比图片,输出结果。
5. 总结:
总体来说, flutter driver 与 Flutter 项目相互支持得非常好,习惯了写法以后,操作起来还是比较快速的,但是由于目前并不是十分成熟,功能有限,然而对于项目上的大多数场景,完全是足够的。采用功能测试和视觉测试的搭配,不仅能真实的测试前后端,还能捕捉到容易被忽视的细节和改动,是一个非常好的实践,如果读者有什么建议和意见,欢迎沟通。