原帖:http://andward.coding.me/flutter/test/2019/01/22/dig-with-flutter-driver.html
Flutter 1.0 问世后关注度越来越高,在 github 上已经 5 万颗星星了。前些日子看了下它的 test tutorial 已然支持了 integration test -> flutter driver。起初看名字以为是 flutter 版的 wedriver(再包装),研究了才发现另有一番天地。
Flutter 的 test 分三个级别:
Unit test 调用的是'package:test'。原文:The package lets you run your tests in a local Dart VM with a headless version of the Flutter Engine, which supplies these libraries. Using this command you can run any test, whether it depends on Flutter libraries or not. Unit 跑测试时 default option 是 Dart VM, 也可以选择跑在某一个 browser 或者 platform。具体介绍可以看测试文档。
Widget test 是 UT 的升级版,调用的'package:flutter_test'。Widget 组件是 flutter 的布局核心(万物皆是 widget),因此 Widget test 在 flutter test 中占有重要的地位。Widget test 在 VM(没错,widget test 也跑在 Dart VM 里面) 通过 flutter engine render 一个交互的可视化组件 (UI) 直接测试渲染和交互效果,准确性和效率都能得到保证。
Integration test 是 widget test 的拓展版(后文分析),调用的是'package:flutter_driver'。其实整个 flutter app 就是一个大的 widget,所以 integration test 主要聚焦 widget 聚合后的交互表现。flutter driver test build-in 在项目里,运行时 test app 也会在 Dart VM 里面 render,通过 test script 与其交互。
三种 test 对象都 (可以) 在 VM 里面运行,那么怎么跟 VM 互动呢?答案是Dart VM Service Protocol。它封装了 JSON-RPC 2.0,来处理 WebSocket request/response。
我们来看例子。Flutter test 的目录结构大概长这样:
'main.dart'是 app 的入口。Folder 'test_driver'用来专门用来装 integration test。
'tap.dart':
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_todo/main.dart' as app;
void main() {
enableFlutterDriverExtension();
app.main();
}
'tap.dart'是 flutter driver 的 target file,它先 enable 了 FlutterDriverExtension,再启动了 app。enableFlutterDriverExtension() 做了些什么呢:
注册了名为'ext.flutter.driver'的 service extension。它调用了'flutter/foundation/binding.dart'中的 registerServiceExtension(最终是调用 dart/developer/extension 中的 service protocol extension handler),是_DriverBinding 的一个 function。
...
void initServiceExtensions() {
super.initServiceExtensions();
final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors);
registerServiceExtension(
name: _extensionMethodName,
callback: extension.call,
);
}
...
激活 Flutter Driver VM service extension。
...
void enableFlutterDriverExtension({ DataHandler handler, bool silenceErrors = false }) {
assert(WidgetsBinding.instance == null);
_DriverBinding(handler, silenceErrors);
assert(WidgetsBinding.instance is _DriverBinding);
}
...
接收来自 FlutterDriver 的 command 去调用相应的处理。代码太长不贴了,简单的说就是定义了_commandHandlers, _commandDeserializers 和_finders 三个 map 映射来接收 command,去调用它们的映射函数。handle 这个过程的就是 extension.call。所有在 FlutterDriver 中的方法最终都通过 command 映射到 extension 中对应的函数。
'tap_test.dart'是 test script,下面是一个简单的 flow。
'tap_test.dart':
import 'dart:async';
// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Tap test', () {
FlutterDriver driver;
setUpAll(() async {
// Connects to the app
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
if (driver != null) {
// Closes the connection
driver.close();
}
});
test('measure', () async {
// Record the performance timeline of things that happen inside the closure
Timeline timeline = await driver.traceAction(() async {
// Find the press button
SerializableFinder pressButton = find.byTooltip('Increment');
// Scroll down 5 times
for (int i = 0; i < 5; i++) {
// Tap for 300 millis
await driver.tap(
pressButton, timeout: Duration(milliseconds: 300));
}
});
// The `timeline` object contains all the performance data recorded during
// the session. It can be digested into a handful of useful
// aggregate numbers, such as "average frame build time".
TimelineSummary summary = TimelineSummary.summarize(timeline);
// The following line saves the timeline summary to a JSON file.
summary.writeSummaryToFile('tap_performance', pretty: true);
// The following line saves the raw timeline data as JSON.
summary.writeTimelineToFile('tap_performance', pretty: true);
});
});
}
初始化 FlutterDriver 后,driver 做了几件事:
连接 VM 里面运行的 test app。'connect()'中的关键部分如下,它去尝试连接 VMServiceClient,拿到 app 的 isolate(isolate 可以认为是 dart 里面的多线程):
...
final VMServiceClientConnection connection =
await vmServiceConnectFunction(dartVmServiceUrl);
final VMServiceClient client = connection.client;
final VM vm = await client.getVM();
final VMIsolateRef isolateRef = isolateNumber ==
null ? vm.isolates.first :
vm.isolates.firstWhere(
(VMIsolateRef isolate) => isolate.number == isolateNumber);
_log.trace('Isolate found with number: ${isolateRef.number}');
VMIsolate isolate = await isolateRef
.loadRunnable()
.timeout(isolateReadyTimeout, onTimeout: () {
throw TimeoutException(
'Timeout while waiting for the isolate to become runnable');
});
...
在 isolate 中调用 invokeExtension,将 script 里面的 finder/interaction 通过 extension 转化成 command,来调用映射函数完成交互:
...
Future<Map<String, dynamic>> _sendCommand(Command command) async {
Map<String, dynamic> response;
try {
final Map<String, String> serialized = command.serialize();
_logCommunication('>>> $serialized');
response = await _appIsolate
.invokeExtension(_flutterExtensionMethodName, serialized)
.timeout(command.timeout + _rpcGraceTime(timeoutMultiplier));
_logCommunication('<<< $response');
} on TimeoutException catch (error, stackTrace) {
...
} catch (error, stackTrace) {
...
}
...
return response['response'];
}
...
extension 中查找元素都是通过 flutter test 的 find 实现的。它可以在 render tree 中搜索想要的 widget。具体细节可以参考'flutter_test/lib/finder.dart'。
extension 中的交互都是通过 LiveWidgetController 中的方式实现的。具体细节可以参考'flutter_test/lib/controller.dart'
方法 forceGC() 往 VM 发一条 ‘_collectAllGarbage’ 的命令强行 GC。是不是 remote interaction 容易 memory leak?
方法 traceAction() 收集 performance timeline,提供了一个容易的方法 track performance。结果长如下的样子:
{
"average_frame_build_time_millis": 12.4331875,
"90th_percentile_frame_build_time_millis": 18.181,
"99th_percentile_frame_build_time_millis": 160.043,
"worst_frame_build_time_millis": 160.043,
"missed_frame_build_budget_count": 8,
"average_frame_rasterizer_time_millis": 9.003393939393936,
"90th_percentile_frame_rasterizer_time_millis": 9.284,
"99th_percentile_frame_rasterizer_time_millis": 129.851,
"worst_frame_rasterizer_time_millis": 129.851,
"missed_frame_rasterizer_budget_count": 4,
"frame_count": 32,
"frame_build_times": [
160043,
6363,
20967,
...
],
"frame_rasterizer_times": [
129851,
15393,
...
]
}
有待更深入发掘...