首页
/ Flutter Tools Web 集成测试分片(web.shard)运行机制与源码解析

Flutter Tools Web 集成测试分片(web.shard)运行机制与源码解析

2026-09-07 19:58:49作者:丁柯新Fawn

Flutter 框架仓库的 flutter_tools 测试体系将“慢速 Web 相关测试”单独收敛在 test/web.shard 目录中,这些测试不依赖真实移动设备、也不封闭(non-hermetic),而是以真实 Flutter SDK 为底座、通过 flutter_tester 来驱动 Dart Web 调试服务(DWDS)与 Flutter 工具集成链路。本文基于 packages/flutter_tools/test/web.shard/README.md,结合仓库内该目录下的实际测试源码、flutter_tools 的测试分片约定与 CI 驱动脚本,完整讲解该分片的定位、本地运行方式、测试内容构成、底层驱动机制,以及它为什么必须独立成片、不参与覆盖率统计。

一、分片背景:flutter_tools 测试目录布局

要理解 web.shard,先要看清它在 flutter_tools 测试体系中的位置。在 packages/flutter_tools/README.md 的 “Writing tests” 一节中,官方明确了各测试目录的职责划分:

目录 定位 特性
test/general.shard 工具内部逻辑的封闭单元测试 必须在远小于 2 秒内完成,CI 会对该目录强制 2 秒级超时
test/commands.shard 工具命令(command)测试 内部再分为 hermetic/(封闭)与 permeable/(非封闭)两个子目录
test/integration.shard 集成测试 典型形态是把工具以**子进程(subprocess)**方式拉起再驱动验证
test/web.shard 慢速 Web 相关测试 即本文主题;本质是“以 Web 为目标平台的集成测试”

web.shardintegration.shard 在形态上是同源的:它们都把 flutter 工具作为黑盒子跑在子进程中,而不直接 import 工具内部实现做白盒断言。差别在于运行目标平台:integration.shard 以真实设备/主机为目标,web.shard 专门覆盖 Chrome 设备与 Web Server 设备这两类 Dart Web 调试目标。

二、web.shard 到底是什么:非封闭的黑盒集成测试

web.shard/README.md 第一段就点明了这类测试的三个关键特征:

  1. Non-hermetic(非封闭):它们不使用 mock 或内存替身,而是使用真实的 Flutter SDK,因此运行结果高度接近开发者在本机执行 flutter run / flutter test 的真实体验。
  2. 不需要真实设备:与 integration.shard 中需要 Android/iOS 真机或模拟器的用例不同,Web 目标跑在桌面浏览器引擎与 flutter_tester 之上。
  3. 测试对象是 DWDS 与 Flutter 集成的整条链路:工具启动 Web 调试会话时,会经由 Dart Web Debug Service(DWDS)对外提供调试能力(如热重载、热重启、断点调试、表达式求值)。web.shard 的用例正是围绕这条链路的端到端行为做验证。

packages/flutter_tools/README.md 的注释可以进一步确认定位:

Integration tests (e.g. tests that run the tool in a subprocess) go under test/integration.shard. Slow web-related tests go in the test/web.shard directory.

也就是说,web.shard 是“集成测试”这一大类的 Web 特化子集,二者共享同一套子进程式驱动框架(见后文 FlutterRunTestDriver 部分),并在 CI 编排与源码组织上彼此复用测试数据。

三、如何在本地运行 web.shard

README 给出了唯一官方运行方式。前提是仓库根目录下已具备一次可用的 SDK 缓存(即 bin/cache 已就绪,包含 dart-sdk 产物)。进入 flutter_tools 包目录后执行:

cd packages/flutter_tools
../../bin/cache/dart-sdk/bin/dart test test/web.shard

几个值得展开的细节:

  • 为什么用 ../../bin/cache/dart-sdk/bin/dart 而不是系统 dart:因为该分片要求真实 Flutter SDK(非封闭测试的第一特征),所以要确保被测试的 flutter 工具、DWDS、以及 SDK 内置引擎版本彼此匹配,直接使用仓库 bin/cache 中下载好的 Dart SDK 最稳妥。
  • 为什么不是 flutter testflutter_tools 自身是 Dart 包,测试由 package:test 驱动;对其内部测试统一用 dart test <shard路径> 的方式执行,与仓库中其它 shard 的跑法保持一致(见 dev/bots/test.dart 中各 runner 对 runDartTest 的调用方式)。
  • 运行时间成本:README 明确提示 “These tests are expensive to run”,它们会反复拉起 flutter 子进程、启动 Chrome/Web Server 会话、执行热重载与热重启等操作,单条用例的成本远高于普通单元测试。
  • 依赖条件:Chrome 设备相关的用例(见下节文件清单中大量 *_chrome_test.dart)需要环境里存在可被驱动、且能与工具链版本匹配的 Chrome/Chromedriver;而 *_web_server_test.dart 一类的用例则运行在 Web Server 设备上,可通过 HTTP 拉取产物做断言,依赖更轻。README 说“they don't require actual devices”,指的是不需要移动端真机/模拟器,并非完全免依赖——具体设备类别的支持情况以 web.shard 目录内测试文件 为准。

若想只跑分片中的单条用例,可在目录后追加 -n <正则> 过滤,例如:

../../bin/cache/dart-sdk/bin/dart test test/web.shard -n "hot restart"

四、测试内容概览:从 19 个测试文件看覆盖范围

test/web.shard 目录共 19 个 *_test.dart 文件与 1 个 test_data/ 共享目录。从文件命名与源码结构看,覆盖主题可分为三大类:

4.1 热重载 / 热重启(Hot Reload / Hot Restart)——核心覆盖区

Chrome 设备侧:

Web Server 设备侧:

从已读源码可以看到典型的单文件结构(hot_reload_chrome_test.dart):

@Tags(<String>['flutter-test-driver'])
library;

import '../integration.shard/test_data/hot_reload_test_common.dart';
import '../src/common.dart';

void main() {
  testAll(chrome: true, additionalCommandArgs: <String>['--no-web-resources-cdn']);
}

要点:用例主体 testAll(...) 复用了 integration.shard 的共享测试数据(test_data/hot_reload_test_common.dart),通过 chrome: true--no-web-resources-cdn 等参数区分设备与运行细节——这正是前文所述“web.shard 与 integration.shard 同源、共享驱动框架”的直接证据。

4.2 Web 调试服务(DWDS / DDS)与 IDE 特性

  • vm_service_web_test.dart:直接面向 DWDS/DDS 的 VM Service 协议做断言。例如其中有一条针对 flutter run--dds-port 参数回归测试(回归来源见文件中注释引用的 issue #159157):先申请一个空闲端口,再以 device: GoogleChromeDevice.kChromeDeviceId 拉起 flutter run --dds-port <port>,最后断言 flutter.vmServicePort == ddsPort。该文件还通过 vmServiceConnectUri 连接 VM Service WebSocket,并用 validateFlutterVersion 验证“Flutter 运行版本可被校验”这一服务能力。
  • web_driver_service_test.dart:围绕 WebDriver 服务(驱动 Chrome 的调试通道)做验证。
  • debugger_stepping_web_test.dartexpression_evaluation_web_ddc_library_bundle_test.dart:覆盖 DDC(Dart Development Compiler)产物下调试器单步执行与表达式求值行为。

4.3 运行 / 构建产物与参数注入

  • web_run_chrome_test.dartweb_run_web_server_test.dartflutter run 在两类 Web 设备上的基础运行链路。
  • web_define_run_test.dart:验证 --web-define 参数注入。从该文件源码可以看到它通过 WebServerDeviceTestRunner 启动 Web Server 设备,然后 _fetch(appUrl) 拉取真实 HTTP 响应的 index.html 正文,断言占位符已被替换(例如 {{MY_VERSION}} 已被替换成真实值、且正文中不再残留占位符模板),并在一次热重载与一次热重启之后重复拉取、验证替换结果仍然保持。其 web 服务端共用的抓取逻辑被抽在 test_data/web_server_test_common.dart
  • output_web_test.dart:校验工具在 Web 会话中的输出文本。
  • chrome_test.dart:围绕 Chrome 设备本身的能力测试。

4.4 共享测试数据目录 test_data/

test_data/ 将跨用例复用的“真实工程样本”与“设备驱动公共代码”集中存放,例如:

  • hot_reload_index_html_samples.darthot_restart_chrome_test_common.dart:供各 Chrome 测试共同执行的用例主体。
  • web_server_test_common.dart:Web Server 设备跑法公共逻辑(WebServerDeviceTestRunner 一类封装所在)。
  • expression_evaluation_web_common.dart:表达式求值用例主体。
  • hot_action_outside_lib_chrome_test_common.dart:针对 lib 目录之外代码变更触发热动作的专项场景。

这种“外壳测试文件极薄、主体逻辑收敛到 test_data”的组织方式,与 integration.shardtest_data/ 设计完全一致,进一步印证两者共享同一套工程方法论。

五、源码级剖析:测试如何驱动 Flutter 子进程

vm_service_web_test.dart 的脚手架为例,可以看清一条完整用例的生命周期:

setUp(() async {
  tempDir = createResolvedTempDirectorySync('run_test.');
  await project.setUpIn(tempDir);
  flutter = FlutterRunTestDriver(tempDir);
});
  • createResolvedTempDirectorySync 在系统临时目录创建工程沙箱;
  • project.setUpIn(tempDir) 将一个真实的最小 Flutter Web 工程模板(如 BasicProjectWithUnaryMain,来自 integration.shard/test_data/basic_project.dart)灌入沙箱——再次体现“使用真实 SDK、非封闭”的特点;
  • FlutterRunTestDriver(tempDir) 负责以子进程方式把 flutter run 拉起来,并向测试暴露 run(...)stop()hotReload()hotRestart()vmServicePortvmServiceWsUri 等句柄;
  • tearDown 中统一 flutter.stop()tryToDelete(tempDir),保证不残留僵尸进程与临时工程。

正是这种黑盒子进程驱动的形态,决定了 README 中关于覆盖率的结论:这些测试“do not give meaningful coverage information for the flutter tool”,因为覆盖率需要插桩到工具进程内部,而黑盒测试无法反映工具内部哪一行被命中;因此它们被排除在覆盖率统计之外。

另外一个值得注意的机制是 @Tags(<String>['flutter-test-driver'])。该 tag 在 packages/flutter_tools/dart_test.yaml 中有登记说明:它标识“测试会调用 flutter test / flutter run 做集成”。同一文件把整个 flutter_tools 测试默认超时设为 15m(对绝大多数用例足够宽松,而 general.shard 的 2 秒紧约束由 dev/bots/test.dart 单独覆盖)。另外 --no-web-resources-cdn 在多数用例中作为 additionalCommandArgs 传入,作用是指示工具从本地资源而非 CDN 拉取 Web 产物,从而保证测试的可控性与离线可复现性。

六、CI 集成:为什么单独成片、怎么切片

README 明确该分片“are in a separate shard when running on continuous integration and are not run when calculating coverage”。在 CI 侧的具体实现位于 dev/bots/test.dart_runWebToolTests

Future<void> _runWebToolTests() async {
  final List<File> allFiles = Directory(
    path.join(_toolsPath, 'test', 'web.shard'),
  ).listSync(recursive: true).whereType<File>().toList();
  final allTests = <String>[];
  for (final file in allFiles) {
    if (file.path.endsWith('_test.dart')) {
      allTests.add(file.path);
    }
  }
  await runDartTest(
    _toolsPath,
    forceSingleCore: true,
    testPaths: selectIndexOfTotalSubshard<String>(allTests),
    includeLocalEngineEnv: true,
  );
}

该 runner 的关键设计:

  • 递归扫描 test/web.shard 下所有 *_test.dart,自动发现用例,新增用例文件无需改动 CI 脚本;
  • forceSingleCore: true:串行执行,避免多条 Web 会话互相抢占端口与浏览器资源——这与用例的昂贵属性直接相关;
  • selectIndexOfTotalSubshard:把全量用例按预设子分片切分,便于在 CI 上把 web.shard 横向拆成多台机器并行、缩短整体耗时;
  • includeLocalEngineEnv: true:允许通过 FLUTTER_LOCAL_ENGINE / FLUTTER_LOCAL_ENGINE_HOST 等环境变量把本地自编译引擎注入测试(相关约定可参考 packages/flutter_tools/README.md)。

由此可以归纳“单独成片、排除出覆盖率”的三条理由:

  1. 耗时昂贵:串行启动真实 Web 会话,若混入普通 shard 会拖垮整体节奏;
  2. 覆盖率无意义:黑盒子进程方式拿不到工具内部行的命中数据;
  3. 环境要求特殊:需要真实 SDK、浏览器/调试服务等运行前提,独立分片便于在 CI 上单独配置这类机器并做子分片扩容。

七、编写与维护注意事项

结合 packages/flutter_tools/README.md 的测试写作约定与 web.shard 的既有实践,给需要为该目录新增用例的开发者如下清单:

  • 先判断归属:能写进 general.shard 的封闭单元测试不要放进 web.shard;只有“慢速、以真实 Web 会话为验证对象”的用例才应落入本目录(归属总则见上文目录布局表格)。
  • 优先复用共享代码:外壳文件保持极薄,把用例主体下沉到 test_data/(如 hot_restart_chrome_test_common.dart 的模式),或直接复用 integration.shardtest_data/*_common.dart
  • 规范传参:Chrome 设备用例通常带上 --no-web-resources-cdn;需要自定义参数的场景经 additionalCommandArgs 传入;涉及端口类回归(如 DDS 端口)优先实测而非假设。
  • 正确打 tag:调用 flutter run / flutter test 的用例必须声明 @Tags(<String>['flutter-test-driver']),与 dart_test.yaml 的约定保持一致。
  • 做好清理tearDown 中务必 flutter.stop() 并删除临时工程目录,避免子进程与文件句柄泄漏。
  • 本地先跑通再提交:用第三节的 dart test test/web.shard 命令做最小验证,并评估运行耗时与 CI 子分片的负载平衡。

八、小结

web.shard 是 Flutter 工具链测试矩阵中面向“真实 Dart Web 调试链路”的黑盒集成测试阵地:它以真实 Flutter SDK 与 flutter_tester 为底座、通过子进程驱动 flutter 工具,覆盖了 Chrome 与 Web Server 两类设备上的热重载、热重启、调试服务、参数注入等核心行为;因为昂贵且无法产出有意义的工具覆盖率,它被 dev/bots/test.dart 独立切分为 CI shard 并排除在覆盖率统计之外。理解它的运行命令、目录结构与驱动机制,既能帮助你在本地快速复现与排查 Web 工具链问题,也是为 Flutter 工具新增 Web 相关回归用例时的必备前置知识。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388