Flutter Tools Web 集成测试分片(web.shard)运行机制与源码解析
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.shard 与 integration.shard 在形态上是同源的:它们都把 flutter 工具作为黑盒子跑在子进程中,而不直接 import 工具内部实现做白盒断言。差别在于运行目标平台:integration.shard 以真实设备/主机为目标,web.shard 专门覆盖 Chrome 设备与 Web Server 设备这两类 Dart Web 调试目标。
二、web.shard 到底是什么:非封闭的黑盒集成测试
web.shard/README.md 第一段就点明了这类测试的三个关键特征:
- Non-hermetic(非封闭):它们不使用 mock 或内存替身,而是使用真实的 Flutter SDK,因此运行结果高度接近开发者在本机执行
flutter run/flutter test的真实体验。 - 不需要真实设备:与
integration.shard中需要 Android/iOS 真机或模拟器的用例不同,Web 目标跑在桌面浏览器引擎与flutter_tester之上。 - 测试对象是 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 thetest/web.sharddirectory.
也就是说,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 test:flutter_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 设备侧:
- hot_reload_chrome_test.dart
- hot_restart_chrome_test.dart
- hot_reload_chrome_errors_test.dart
- hot_reload_outside_lib_chrome_test.dart
- hot_restart_outside_lib_chrome_test.dart
- hot_reload_with_asset_chrome_test.dart
- stateless_stateful_hot_reload_web_test.dart
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.dart 与 expression_evaluation_web_ddc_library_bundle_test.dart:覆盖 DDC(Dart Development Compiler)产物下调试器单步执行与表达式求值行为。
4.3 运行 / 构建产物与参数注入
- web_run_chrome_test.dart 与 web_run_web_server_test.dart:
flutter 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.dart、hot_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.shard 的 test_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()、vmServicePort、vmServiceWsUri等句柄;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)。
由此可以归纳“单独成片、排除出覆盖率”的三条理由:
- 耗时昂贵:串行启动真实 Web 会话,若混入普通 shard 会拖垮整体节奏;
- 覆盖率无意义:黑盒子进程方式拿不到工具内部行的命中数据;
- 环境要求特殊:需要真实 SDK、浏览器/调试服务等运行前提,独立分片便于在 CI 上单独配置这类机器并做子分片扩容。
七、编写与维护注意事项
结合 packages/flutter_tools/README.md 的测试写作约定与 web.shard 的既有实践,给需要为该目录新增用例的开发者如下清单:
- 先判断归属:能写进
general.shard的封闭单元测试不要放进 web.shard;只有“慢速、以真实 Web 会话为验证对象”的用例才应落入本目录(归属总则见上文目录布局表格)。 - 优先复用共享代码:外壳文件保持极薄,把用例主体下沉到
test_data/(如hot_restart_chrome_test_common.dart的模式),或直接复用integration.shard的test_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 相关回归用例时的必备前置知识。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00