首页
/ Flutter 框架 Golden File 测试实战:Flutter Gold 基线管理与 CI 流水线完整工作流

Flutter 框架 Golden File 测试实战:Flutter Gold 基线管理与 CI 流水线完整工作流

2026-09-06 13:50:16作者:温玫谨Lighthearted

本文围绕 Flutter 仓库中 package:flutter 的 golden file(黄金文件/截图)测试体系展开,系统讲解如何配置 .ci.yaml 任务、编写并命名 golden 测试、通过 --update-goldens 迭代本地图像、在 Flutter Gold 的 ChangeLists tryjob 中完成图像 triage(分诊),以及 flutter-gold 检查与 reduced-test-set 标签的底层机制。读完后,你可以在 flutter/flutter 仓库中独立新增、更新 golden 测试,并理解其预提交/后提交/本地三种比较策略的源码实现原理。

package:flutter 的 Golden 测试为什么特殊

对普通业务包而言,写 golden 测试只需要 testWidgets 配合 matchesGoldenFile 断言即可(其 API 文档是面向外部包的首选参考)。但对 package:flutter 本身,情况完全不同:

  • 基线不存放在仓库中package:flutter 的 golden file 使用 Flutter Gold(Skia Gold 的一个实例)做基线与版本管理,而不是把参考图 checked in 到代码仓库。这样做的直接收益是:同一份测试代码可以在 Linux、Windows、macOS 和 Web 四个平台上分别持有基线,从容容纳各平台之间偶发的细微渲染差异——一个测试可能对应多份 golden master,每个基线用一组参数字段(platform、CI 环境、测试名、浏览器、图片扩展名等)做区分。
  • 比较策略随环境切换。仓库内 flutter_goldens.dart 根据运行环境自动选择不同比较器(下文深入剖析),开发者无需手动干预。
  • 流程有严格的配套要求。漏掉其中任何一步(例如 .ci.yaml 未加 goldctl 依赖、任务名未注册到 flutter-gold 检查),都会导致 build 破损或测试莫名失败。

创建一个新的 Golden File 测试

为一个尚未配置 golden 测试的测试套件(test suite / shard)新增 golden 测试共三步,顺序不能乱:

  1. 新增或配置 .ci.yaml 任务;
  2. 编写并提交测试;
  3. 把任务名加入 flutter-gold 检查的已知任务列表。

第一步:为 .ci.yaml 任务添加 goldctl 依赖

每个真正执行测试(含 golden 测试)的测试套件都必须在仓库顶层的 .ci.yaml 中配置——该文件集中定义了 Flutter 仓库全部 CI 任务。如果你只是编辑一个已经带有 goldctl 依赖的既有测试套件,可以跳过本步。

以任务 Linux framework_tests_widgets 为例,它在 .ci.yaml 中的实际配置如下(节选):

- name: Linux framework_tests_widgets
  recipe: flutter/flutter_drone
  timeout: 60
  properties:
    dependencies: >-
      [
        {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"}
      ]
    shard: framework_tests
    subshard: widgets
    tags: >
      ["framework","hostonly","shard", "linux"]
  runIf:
    - dev/**
    - packages/flutter/**
    - packages/flutter_driver/**

语义是:该任务在安装工具链时会拉取指定 git revision 的 goldctl 工具(goldctl 是与 Skia Gold 交互的命令行客户端)。如果是新增测试套件,或为原来没有 golden 测试的套件开启 golden 能力,需要按下述方式补上依赖:

+  properties:
+    dependencies: >-
+      [
+        {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"}
+      ]

提示:goldctl 的具体 git revision 应当直接从 .ci.yaml 中某个既有的、已带该依赖的任务里复制,保证与现有任务一致。

第二步:编写测试

测试本身按常规写法

像写普通 widget 测试一样,使用 testWidgetsawait tester.pumpWidget 等常规手段把被测子树搭起来。

用 RepaintBoundary 圈定验证范围

在你想验证的那段子树外包裹一个 RepaintBoundary。如果不包,输出将是一张 2400×1800 的整屏图片——因为测试默认使用 800×600 的视口、设备像素比为 3.0。若还想进一步控制图片尺寸,可以再用 SizedBox 包住 RepaintBoundary 施加约束。

添加 matchesGoldenFile 断言

await expectLater(
  find.byType(RepaintBoundary),
  matchesGoldenFile('test_name.subtest.subfile.png'),
);

matchesGoldenFile 的实参就是截图的文件名,命名必须遵守三段式约定:

命名要求
第一段(第一个点之前) 必须精确匹配测试文件名。例如测试文件是 widgets/foo_bar_test.dart,则用 foo_bar
subtest 必须在该文件内对每一个 testWidgets 条目唯一
之后的段 必须在同一个 testWidgets 条目内唯一

这套命名让每个测试文件里可以有多个 testWidgets,各自拥有独立的图片命名空间;同一条目内若有多张截图也能互相区分。注意:文件名必须以 .png 结尾——从源码看,flutter_goldens.dart_addPrefix 方法里有断言强制要求,否则本地比较会直接报错(Skia Client 侧同样期望该扩展名)。

仓库中的真实用例可以参考 circle_avatar_test.dart:文件第 7 行带有 @Tags(<String>['reduced-test-set']) 标签,其中一条目内先包 RepaintBoundary 再断言:

await expectLater(find.byType(CircleAvatar), matchesGoldenFile('circle_avatar.fallback.png'));

本地生成与验证图片

写好测试后,在 flutter 包目录下执行:

flutter test --update-goldens test/foo/bar_test.dart

(把路径替换为你新增测试的相对路径。)该命令会把图片更新到 bin/cache/pkg/skia_goldens/packages/flutter/test/ 下,其子目录层级与 fluttertest 目录的目录结构一一对应。这与源码中的实现一致:flutter_goldens.dartgetBaseDirectory 方法以 FLUTTER_ROOT 环境变量为基准,把比较目录定位到 bin/cache/pkg/skia_goldens 之下。请逐张检查生成的图片是否符合预期,不满意就改测试、重复执行,直到满意为止。

一个容易困惑的点:本地不带 --update-goldens 直接 flutter test 时,新测试会直接通过——因为 Flutter Gold 上还没有基线可比较,测试会被识别为“新测试”,系统只输出待验证的图片。真正的图像把关发生在后文的 tryjob triage 环节。

在 Skia Client 中新增 key 的注意事项

Flutter Gold 上的已批准图像按一组参数做 key(platform、CI 环境、测试名、浏览器、图片扩展名等)。如果你要往这套 key 里加新字段,必须考虑该字段的全部可能取值是否都被 pre-submit / post-submit 所用的 CI 环境覆盖——覆盖不全会在本地测试中产生假阴性(false negative)。

文档给出了一个真实教训:曾经引入过一个 abi key,当时 CI 环境里取值只有 linux_x64windows_x64mac_x64 三种;这些 key 被用于本地测试时查找已批准图像,于是当 mac_arm64 机器跑本地测试时查不到任何图像,测试直接失败。省略该 key 查找大部分时候能命中正确图像但并不稳定可靠,最终团队选择了移除该 key。结论是:如果现有 CI 环境无法覆盖某 key 的全部取值,这个 key 就不适合纳入测试参数。

提交 PR 与 tryjob 分诊

本地图片满意后即可提交 PR 评审。评审人同样需要核对你生成的 golden 图像,因此务必把 golden 图片放进 PR 描述里

新测试的结果会被编译成 Flutter Gold 仪表板 ChangeLists 页面下的一个 tryjob:在那里可以看到你的 PR 及关联的 golden 文件,Gold 也会在你的 PR 上留评论并附上指向图像结果的链接。在 tryjob 中对新测试做 triage 会使处于 pending 状态的 flutter-gold 检查通过。评审 tryjob 与生成图像时要确认它们符合预期——目前 Linux、macOS、Windows、Web 四个平台都会生成图像,平台之间存在轻微差异属正常现象。点击对勾批准(approve)即完成 triage。

至此,你的新 golden 文件会作为新测试的基线被 checked in,PR 可以合并。

第三步:把任务名加入 flutter-gold 检查

flutter-gold 检查是一个每 5 分钟一次的 cron 作业:它扫描 flutter/flutterflutter/engine 上所有非 draft 的 open PR,找出其中已知会跑 golden 文件测试的任务(task);如果命中,就通过 Skia Gold API 查询是否产生了未 triage 的图像——有则显示问号状态,无则显示勾。

这份“已知任务名”列表人工维护flutter/cocoon 代码库的 app_dart/lib/src/request_handlers/push_gold_status_to_github.dart 中,新增或下线任务时必须同步更新。漏掉这一步的直接后果是:部分 PR 上的 flutter-gold 检查根本不跑,进而酿成 build 破损。

源码纵深:四种比较器如何按环境自动切换

理解 flutter_goldens.darttestExecutable 入口的实现,能解释上面流程中“为什么本地过、CI 才较真”的现象。testExecutable 会在 flutter_test_config.dart 被调用(package:flutter 的入口是 flutter_test_config.dart,它把执行委托给 flutter_goldens.testExecutable)时,按当前环境四选一安装比较器:

比较器 生效条件(源码判定逻辑) 行为
FlutterPostSubmitFileComparator 存在 SWARMING_TASK_IDGOLDCTL不存在 GOLD_TRYJOB,且 GIT_BRANCHmain/master 通过 SkiaGoldClientimgtestInit/imgtestAdd 把图像直接上传 Gold 做后提交验证
FlutterPreSubmitFileComparator 存在 SWARMING_TASK_IDGOLDCTLGOLD_TRYJOB,且 GIT_BRANCHmain/master 通过 tryjobInit/tryjobAdd 把图像送入 tryjob;compare 恒返回 true,失败判定交给 flutter-gold 状态检查
FlutterSkippingFileComparator 存在 SWARMING_TASK_ID(LUCI 环境但其余条件不满足,例如未配置 goldctl 的分片) 直接跳过,仅打印跳过原因
FlutterLocalFileComparator 以上都不满足(本地开发) 向 Gold 请求当前参集合匹配的基线做本地像素比对;查不到基线则视为新测试,写出图片并直接返回通过;有差异则生成可视 diff 并抛 FlutterError

几个值得注意的实现细节:

  • 本地无法连 Gold 时自动降级FlutterLocalFileComparator.fromLocalFileComparator 会先做一次探活请求,捕获 OSError/SocketException/FormatException 后降级为 FlutterSkippingFileComparator——这就是本地断网时 golden 测试不会硬失败的原因。
  • CI 分片目录随机化。后提交/预提交比较器给 base 目录加随机后缀(flutter_goldens_postsubmit. / flutter_goldens_presubmit. 前缀的临时目录),以在使用 goldctl 时保持线程安全。
  • 图像 URL 拼接规则_addPrefix 会把“测试文件所在目录名 + golden 文件名”拼成最终 key,namePrefix(在 testExecutable(namePrefix:) 传入)再前置拼接——这就是三段式命名约定能映射到 Gold 上唯一图像身份的底层原因。

更新既有的 Golden File Test

当渲染结果发生变化,Flutter Gold 上的基线就需要更新。开发过程中的体验与新增测试不同:

  • 本地:测试会产生失败,并附带可视 diff 输出。该 diff 使用的是 Gold 上与你当前测试环境参集合(目前主要按 platform 区分)匹配的基线,方便快速迭代验证。在新基线被 checked in 之前,本地测试不会通过。
  • 提交后:同样把 golden 变更写进 PR 描述供评审人核对。测试变更会被编译进 ChangeLists 下的 tryjob,其中能看到新旧基线之间的可视差异;Gold 也会在 PR 上留评论。
  • triage:在 tryjob 中审看并点击对勾批准图像(四平台的轻微差异属正常),flutter-gold 检查随即转绿,基线更新完成。

Flutter Gold 登录与权限

Triage 权限目前仅限 flutter-hackers 组的成员。获得授权后,可通过 Flutter Gold 仪表板首页登录,然后在 Changelists 页面对自己的图像结果执行 triage。更多权限细节见 Contributor Access

flutter-gold 检查的工作机制

flutter-gold 检查作用于 flutter/flutter 上“会执行 golden 文件测试且已就绪(ready for review)”的 PR。要点:

  • 等待全部测试完成:golden 测试分布在多个测试分片上,该检查会等其他测试全部跑完,再确认 Gold 是否收到了新图像。等待期间以及存在图像变更时,检查保持 pending 状态。这是 flutter/engine 的 auto-roller 工作方式决定的(相关背景见上游 issue #48744)。
  • 无图像变更:检查直接变绿。
  • 检测到图像变更:PR 上会出现通知评论并附直达图像的链接;完成 triage/批准后,检查会在 5 分钟内转绿。

reduced-test-set 标签

在部分 CI 平台的 pre-submit 中,为了节约资源、加快其他变更的测试速度,hermetic 测试套件不会全量执行。为保证每个平台都能拿到 golden 图像,含 golden 测试的文件要打 reduced-test-set 标签,标记这些文件应在这类“精简测试环境”中执行——目前 Mac 和 Windows 平台上的 framework 测试执行的就是这个精简集合。标签必须按如下形式置于文件顶部:

@Tags(<String>['reduced-test-set'])

该标签在仓库中有三处相互印证的落点:

  1. 标签声明dart_test.yaml 中声明了 reduced-test-set 标签(同文件还声明了 no-shuffle 标签);
  2. 真实使用:如 circle_avatar_test.dart 第 7 行,顶部注释明确写着“This file is run as part of a reduced test set in CI on Mac and Windows machines.”;
  3. 静态检查强制:仓库自带的分析器插件规则 golden_test_tags.dart 定义了 golden_test_tags 规则——凡是路径包含 packages/flutter/test 的文件中出现了 matchesGoldenFile 调用,却没有在 import 之前带 @Tags(<String>['reduced-test-set']),就会以 ERROR 级别报错。规则的纠正文本正是指回本文档,测试用例见 golden_test_tags_test.dart

已知问题:Negative Images

如果某张图像被 Gold 标记为 negativeflutter-gold 检查在 pre-submit 阶段不会捕获它——该系统依赖 untriagedapproved 状态工作。因此图像不应被标记为 negative。若 post-submit 阶段产生了 negative 图像,测试会失败,此时应当回滚(revert)相关变更以便处理该图像。

如果你希望立即作废旧有全部渲染结果,最简单的方式是改掉 golden file 测试的名字(图像身份由命名决定)。Gold 本身已有“遗忘”已变更图像的机制,但它是渐进式的。

Build 破损与故障排查

后提交阶段图像未 triage 导致的 build 破损

如果 Flutter build 因 golden 文件测试失败而破损,通常意味着一次图像变更未经 triage 就落了地。正确流程要求 golden 图像在 pre-submit 阶段(即 PR 评审期间)完成 triage;流程被跳过时,带未批准图像的测试会在 post-submit 测试中失败,错误信息形如:

  Skia Gold received an unapproved image in post-submit
  testing. Golden file images in flutter/flutter are triaged
  in pre-submit during code review for the given PR.

  Visit https://flutter-gold.skia.org/ to view and approve
  the image(s), or revert the associated change.

处理方式:到 Flutter Gold 仪表板查看相关批次的图像。若是预期内的变更,点击对勾批准,然后重跑失败的测试即可解决;若非预期变更,回滚对应提交。

另需注意:仪表板上的“归因(blame)”可能不准——flutter/flutter 的 post-submit 测试并非按提交落地顺序执行,而是按最近变更排序测试。在旧提交的结果尚未全部处理完之前,Gold 可能把图像变更错误地归因到别的提交。

常见排障清单

症状 原因与解法
其他检查全绿,flutter-gold 卡在 pending 可能是 force-push(git push -f)的副作用,也是 Skia Gold 的已知问题(通常在 rebase 之后出现,且行为 flaky)。尝试重新 rebase,或不要 force push 地推一个空提交
检查留言 “Golden file changes have been found...”,但 triage 页面是空的 可能是 force-push 的又一副作用,也可能是已合并 PR 遗留的未 triage golden。先尝试再次 rebase,仍无解则到 Discord 的 #hackers-infra 频道求助

关键路径速查

内容 仓库路径
本文档 Writing-a-golden-file-test-for-package-flutter.md
CI 任务与 goldctl 依赖 .ci.yaml
比较器选择与实现 flutter_goldens.dart
框架测试的 golden 接线 flutter_test_config.dart
reduced-test-set 标签声明 dart_test.yaml
强制标签的分析器规则 golden_test_tags.dart
带 golden 断言的真实用例 circle_avatar_test.dart

最后提醒一点适用边界:本文流程仅适用于为 package:flutter 编写 golden 测试。若你在写集成测试,或在 flutter/engine 等其他仓库写测试,细节会有所不同,应查阅对应仓库的测试文档(如 engine 下 testing/skia_gold_clientimpeller/golden_tests 的 README)并咨询相应团队。

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