通用技术 Flutter driver 初探

andward_xu · 2019年01月23日 · 最后由 linkin 回复于 2020年05月09日 · 2619 次阅读

原帖: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。单元测试,函数级别的逻辑验证 (no-UI)。
  • Widget test。组件测试,组件的交互测试。
  • Integration test。集成测试,app 内 widgets 的集成测试

说重点:

三种测试基础环境是一样的

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。

Service extension 模式

我们来看例子。Flutter test 的目录结构大概长这样:

  • lib
    • main.dart
  • test_driver
    • tap.dart
    • tap_test.dart

'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,
    ...
  ]
}

有待更深入发掘...

共收到 1 条回复 时间 点赞

挺好,新领域,一起探究

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册