首页
/ Flutter 集成测试套件实践:dev/integration_tests 的两种形态、本地调试与 Devicelab 运行机制

Flutter 集成测试套件实践:dev/integration_tests 的两种形态、本地调试与 Devicelab 运行机制

2026-09-06 16:17:50作者:裘晴惠Vivianne

dev/integration_tests 是 Flutter 仓库中专用于真机自动化集成测试的目录:它存放的每个测试套件要么是"完整 Flutter 应用 + flutter_driver 驱动规范"的组合,要么是"为测试 Flutter 集成而准备的原生宿主应用"。这些套件主要服务于 Devicelab 真机实验室,开发者也可以在本地用 flutter drive 命令直接调试单个套件。读完本文,你将理解该目录下两类套件的差异、如何在本地运行与调试一个 driver 测试、Devicelab 框架在底层如何包装 flutter driveflutter test,以及新增测试时 CI 注册的约束。

测试套件的两种形态

根据 dev/integration_tests/README.md 的定义,目录下的每个套件属于以下二者之一:

  1. 完整 Flutter 应用 + flutter_driver 规范:一个可独立运行的 Flutter 应用,配合 test_driver/ 目录下的驱动代码,从 UI 层驱动测试。这是最典型的形态;
  2. 原生宿主应用:一个 native app,用于测试 Flutter 以"嵌入/集成"方式接入宿主环境时的行为(例如 iOS 的 add-to-app、Android 的 hybrid views 场景)。

从目录结构可以印证这一点:像 channelsflavorsplatform_interaction 这类套件都是标准 Flutter 应用加驱动代码;而 ios_add2app_life_cycleios_host_apphybrid_android_views 等则以原生工程为主体。

此外,从仓库中各套件的 README 可以看到,部分套件还承担更细粒度的职责,例如:

本地运行与调试:flutter drive

原 README 给出的核心操作是:想在本地跑一个 driver 测试(比如调试某个挂掉的测试)时,进入对应套件的子目录,执行

flutter drive -t <test> --driver <driver>

原文档给出的实例:

flutter drive -t lib/keyboard_resize.dart --driver test_driver/keyboard_resize_test.dart

这个例子的真实来源是 ui 套件中的键盘尺寸测试。设备端目标 lib/keyboard_resize.dart 负责渲染被测界面,驱动端 test_driver/keyboard_resize_test.dart 则通过 FlutterDriver.connect() 建立连接后,用 find.byValueKey 定位带 ValueKey 的控件来验证行为。以"键盘弹出/收起时视图是否正确缩放"为例,其核心逻辑是:

  1. 先读取初始状态下高度文本(keys.kHeightText)的文本内容,作为基准高度;
  2. 点击文本框(keys.kDefaultTextField)弹出软键盘,由于检测软键盘开合的唯一实用方式是轮询等待布局变化,测试以 300ms 间隔轮询最多 200 次(约 60 秒),比较键盘弹出后的高度是否小于基准高度;
  3. 点击"取消聚焦"按钮(keys.kUnfocusButton)收起键盘,再验证高度恢复。

驱动代码中值得注意的工程细节是轮询上限的注释:在本地真机上,Pixel 8 Pro (API 36) 通常一次轮询即可观察到布局变化,而较老的 Galaxy Tab S3 (API 28) 需要 2~3 次;由于有 issue 记录显示键盘弹出偶尔最长可耗时 21.3 秒,所以把轮询总窗口放宽到 60 秒。这类注释体现了真机测试对设备差异的容忍策略。

ui 套件的 pubspec.yaml 也展示了 driver 测试套件的典型依赖形态:flutter_driverintegration_test 两个 SDK 包同时引入——test_driver/flutter_driver 路线,integration_test/ 目录则走 integration_test 包路线(Devicelab 对这两条路线分别用 flutter driveflutter test 驱动,后文详述)。

Devicelab 如何驱动这些套件:DriverTestIntegrationTest

这些套件"Intended for use with devicelab tests"(原 README 原话),其背后的执行器是 Devicelab 框架。dev/devicelab/README.md 描述了整体机制:任务声明目标设备类型(linux_androidmac_ios 等),实验室中空闲设备领取任务执行;成功则上报性能指标,失败自动重跑,最终全部失败才上报为失败。

具体到 dev/integration_tests 下的套件,Devicelab 侧的入口集中在 dev/devicelab/lib/tasks/integration_tests.dart,它提供两个核心执行器:

DriverTest:包装 flutter drive

DriverTest 对应上面"完整应用 + driver 规范"形态。其 call() 的执行链为:

  1. 通过 devices.workingDevice 选定设备并 unlock()(或接受外部传入的 deviceIdOverride);
  2. 在套件目录下执行 flutter packages get
  3. 组装参数并执行:
final options = <String>[
  '--no-android-gradle-daemon',
  '-v',
  '-t',
  testTarget,
  '-d',
  deviceId,
  ...extraOptions,
];
await flutter('drive', options: options, environment: env);

也就是说,本地手敲的 flutter drive -t <test> -d <device> --driver ...,在 Devicelab 里由 DriverTest--no-android-gradle-daemon -v 的附加参数自动完成。它还会向驱动进程注入环境变量 FLUTTER_DEVICE_ID_NUMBERFLUTTER_ADB_PATH,让驱动代码(如需操作 ADB 的场景)可以直接引用设备号。

各套件到执行器的映射函数也在这个文件里,例如:

  • createEndToEndKeyboardTest()ui 套件的 lib/keyboard_resize.dart(即上文本地调试的例子);
  • createChannelsIntegrationTest()channels 套件的 integration_test/main_test.dart(走 IntegrationTest);
  • createDisplayCutoutTest()display_cutout_rotation 套件,并在 setup 中通过 cmd overlay enable ... 给设备注入合成刘海、在 tearDown 中移除;
  • dartDefinesTask() → 向 flutter drive 追加 --dart-define 参数验证自定义 define 的透传。

IntegrationTest:包装 flutter test

对于 integration_test 包路线的套件(如 channelsspell_checkui/integration_test),Devicelab 使用 IntegrationTest 执行器,其关键步骤为:

await flutter('packages', options: <String>['get']);
await setup?.call(await devices.workingDevice);
// 可选:为纯 Dart 套件动态生成平台目录
if (createPlatforms.isNotEmpty) {
  await flutter('create', options: <String>['--platforms', createPlatforms.join(','), '--no-overwrite', '.']);
}
final options = <String>['-v', '-d', deviceId, testTarget, ...extraOptions];
await flutter('test', options: options, environment: environment);

相比 DriverTest,它还多了三项能力:

  • setup/tearDown 钩子:在设备对象上执行前置/后置操作(如 display_cutout_rotation 的刘海开关、Android 版本校验 getprop ro.build.version.sdk 要求 API 30+);
  • createPlatforms:对缺少原生工程目录的套件现场执行 flutter create --platforms <list> --no-overwrite .,例如 engine_integration_golden_test 只为 windows 平台补目录;
  • withTalkBack:仅在 Android 设备上启用 TalkBack 以运行 android_semantics_testing 套件,测试结束后自动关闭。

特殊形态:会"改源码"的 keyboard_hot_restart

dev/devicelab/lib/tasks/keyboard_hot_restart_test.dart 展示了集成测试的一种更"重"的玩法。由于该测试必须对应用执行热重启(XCUITest、integration_test 均不支持),它直接使用 flutter run 拉起应用,然后通过解析 stdout 实现状态机:

waitUntilDartVmAvailable → waitUntilKeyboardOpen → waitUntilHotRestart → waitUntilKeyboardClosed

流程是:先把套件源码 lib/main.dart 中的 const bool forceKeyboard = false; 改写为 true,使应用启动即弹出键盘;看到日志 flutter: Keyboard is open 后,再把源码改回 false 并向 stdin 写入 R 触发热重启;确认 Restarted application in ... 后等待 flutter: Keyboard is closed,写入 q 结束。finally 块保证无论成败都会把 main.dart 恢复原内容。这是"原生宿主/工具级"测试的一个典型样本:测试逻辑与 flutter 工具链本身的交互(run、热重启、日志解析)紧密耦合。

CI 集成:新测试需要新的 CI 目标

原 README 最后一节强调了一条重要规则:

Adding code to this directory will not automatically cause it to be run by any already existing ci tooling. This directory is intentionally a "choose your own adventure" piece of tooling.

也就是说,dev/integration_tests 下新增代码不会自动被任何现有 CI 工具运行——该目录有意保持"自选路线"的开放性。要让一个套件真正进入 CI,需要在 .ci.yaml 中新增对应的 target。仓库中现存目标可作参照:

- name: Mac_ios spell_check_test
  ...
  task_name: spell_check_test_ios
- name: Mac keyboard_hot_restart_ios
  ...
  task_name: keyboard_hot_restart_ios
- name: Linux windowing_test
  ...
  task_name: windowing_test_linux

可以看到 target 名按 平台 设备 特性 组织(如 Mac keyboard_hot_restart_iosWindows windowing_test),task_name 则与 Devicelab 任务名对应,最终由 dev/devicelab/lib/tasks/integration_tests.dart 中的工厂函数(如 createSpellCheckIntegrationTest)落到具体套件目录与测试入口文件。按 dev/devicelab/README.md 的说法,测试跨多个操作系统时要为每个操作系统各建一个 target;本地验证则可在 dev/devicelab 目录下用:

../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t {NAME_OF_TEST}

以与 CI 相近的方式运行任务(支持 --exit 跳过自动重试,支持 --local-engine 系列参数针对本地引擎构建验证)。

小结

  • dev/integration_tests 下的套件分两类:Flutter 应用 + flutter_driver 驱动规范,以及用于 Flutter 集成测试的原生宿主应用;它们服务于 Devicelab 真机实验室,也可在本地以 flutter drive -t <test> --driver <driver> 的方式直接调试;
  • 本地调试实例 flutter drive -t lib/keyboard_resize.dart --driver test_driver/keyboard_resize_test.dart 对应 ui 套件的键盘缩放验证,驱动端通过轮询布局变化来检测软键盘开合;
  • Devicelab 侧由 integration_tests.dart 中的 DriverTest(包装 flutter drive)与 IntegrationTest(包装 flutter test,附带 setup/tearDown、平台目录生成、TalkBack 开关等钩子)驱动这些套件;
  • 由于该目录是"choose your own adventure"式工具集,新增测试不会自动接入 CI,必须在 .ci.yaml 中显式新增 target,并按需跨平台各建一份。
登录后查看全文
热门项目推荐
相关项目推荐