bigywater.tech
Java · 376

Golden Master 测试:保护复杂输出的行为

Golden master 能用很少的代码保护复杂输出,但它的价值取决于输出是否确定。这篇用一个真实的小程序量出:哪些行会变、换环境会变多少、加固后变成什么,以及自动更新 master 的风险。

0 / 6
同一台机器重复运行 6 次与 master 逐字一致的次数(时间戳与 UUID)
16 / 18 / 4
de_DE / tr / 美国西部时区 三个环境相对基线不同的输出行数
2 vs 6
同一个 1 行 bug 在加固版与未加固输出里造成的 diff 行数

朴素的 golden master

实测 · 本机执行输出
  master.txt(17 行,509 字节)的前 8 行:
    == INVOICE ==
    generated: 2026-10-03T20:23:51.697400Z
    id: 23def6f1-56e5-463c-9166-6a88163abd6e
    date: March 8, 2026
    zone view: 20:00
    widget         3 x       9.99 =        29.97
    gadget        12 x   1,234.50 =    14,814.00
    thing          1 x       0.30 =         0.30
  同一台机器、同样的参数再运行 6 次,与 master 完全一致的有 0 次
  第 1 次运行与 master 的差异(共 4 行变化,前 4 行):
    -generated: 2026-10-03T20:23:51.697400Z
    -id: 23def6f1-56e5-463c-9166-6a88163abd6e
    +generated: 2026-10-03T20:23:51.776858Z
    +id: 0dabb150-b8da-4ad3-bd4c-7372f1a82de6

哪些行不稳定

实测 · 本机执行输出
  共 17 行,其中 2 行在 12 次运行里出现了不同取值:
    第 2 行:12 种取值,例:generated: 2026-10-03T20:23:52.279745Z
    第 3 行:12 种取值,例:id: 6badfd94-32bd-48bc-8dd0-224cec99e332
  其中 tags 行(用身份哈希的 HashSet)出现了 1 种顺序: ['tags: [epsilon, alpha, beta, gamma, delta]']
  byName 行(String 键的 HashMap)出现了 1 种顺序: ['byName: {widget=3, title case=2, gadget=12, thing=1}']

换一个环境

实测 · 本机执行输出
  [基线] 与基线(忽略 generated / id 两行)相比 0 行不同
  [德语 de_DE] 与基线(忽略 generated / id 两行)相比 16 行不同:
      -date: March 8, 2026
      +date: 8. März 2026
      -widget         3 x       9.99 =        29.97
      -gadget        12 x   1,234.50 =    14,814.00
      -thing          1 x       0.30 =         0.30
      -title case     2 x      20.00 =        39.99
      -TOTAL 14,884.26
      +widget         3 x       9,99 =        29,97
  [土耳其语 tr] 与基线(忽略 generated / id 两行)相比 18 行不同:
      -date: March 8, 2026
      +date: 8 Mart 2026
      -widget         3 x       9.99 =        29.97
      -gadget        12 x   1,234.50 =    14,814.00
      -thing          1 x       0.30 =         0.30
      -title case     2 x      20.00 =        39.99
      -TOTAL 14,884.26
      -title: TITLE / info
  [美国西部时区] 与基线(忽略 generated / id 两行)相比 4 行不同:
      -zone view: 20:00
      +zone view: 05:00
      -tags: [epsilon, alpha, beta, gamma, delta]
      +tags: [beta, gamma, alpha, delta, epsilon]
  [file.encoding=ISO-8859-1] 与基线(忽略 generated / id 两行)相比 2 行不同:
      -charset: UTF-8
      +charset: ISO-8859-1
  [阿拉伯语 ar] 与基线(忽略 generated / id 两行)相比 16 行不同:
      -date: March 8, 2026
      +date: 8 مارس 2026
      -widget         3 x       9.99 =        29.97
      -gadget        12 x   1,234.50 =    14,814.00
      -thing          1 x       0.30 =         0.30
      -title case     2 x      20.00 =        39.99
      -TOTAL 14,884.26
      +widget         ٣ x       ٩٫٩٩ =        ٢٩٫٩٧

加固之后

实测 · 本机执行输出
  加固版输出(9711660e):
    == INVOICE ==
    generated: <ts>
    id: <uuid>
    date: 2026 Mar 8
    zone view: 12:00
    widget         3 x       9.99 =        29.97
    gadget        12 x   1,234.50 =    14,814.00
    thing          1 x       0.30 =         0.30
    title case     2 x      20.00 =        39.99
    TOTAL 14,884.26
    title: TITLE / info
    tags: [alpha, beta, gamma, delta, epsilon]
    byName: {gadget=12, thing=1, title case=2, widget=3}
    cwd-relative: true
    pi: 3.142
    charset: <hidden>
    unicode: café 日本
  [基线] 与加固基线相比 0 行不同
  [德语 de_DE] 与加固基线相比 0 行不同
  [土耳其语 tr] 与加固基线相比 0 行不同
  [美国西部时区] 与加固基线相比 0 行不同
  [file.encoding=ISO-8859-1] 与加固基线相比 0 行不同
  [阿拉伯语 ar] 与加固基线相比 0 行不同
  加固版重复运行 12 次,与基线一致 12 次

一个 1 行变化的 diff

实测 · 本机执行输出
  加固版输出 17 行,引入 bug 后差异 2 行:
    -TOTAL 14,884.26
    +TOTAL 14,884.27
  对比:不加固的输出(含时间戳 / UUID / HashSet 顺序)引入同一个 bug 之后的差异行数: 6 行(其中大部分与 bug 无关)
    -generated: 2026-10-03T20:23:54.730945Z
    -id: a1537cdd-7469-4e97-905d-e826272558c8
    +generated: 2026-10-03T20:23:54.818913Z
    +id: 43280234-e656-4c16-9ad0-7c585a806ea3
    -TOTAL 14,884.26
    +TOTAL 14,884.27

update 开关

实测 · 本机执行输出
  update=false: 失败:-TOTAL 14,884.26 / +TOTAL 14,884.27
  update=true: 不一致,但 update=true:已覆盖 master → 通过
  再运行一次(update=false): 通过 (bug 被写进了 master)
  master 里现在的 TOTAL 行: ['TOTAL 14,884.27']

master 固化缺陷

实测 · 本机执行输出
  加固版输出里有两处值得怀疑的数字:
    thing          1 x       0.30 =         0.30
    title case     2 x      20.00 =        39.99
  "title case" 一行显示 单价 20.00、数量 2、金额 39.99(20.00 × 2 应该是 40.00):格式化单价与金额各自做了舍入。0.1 + 0.2 显示成 0.30,原值是 0.30000000000000004。这两个现象现在都被 master 当作「正确输出」固化下来——master 只能说明输出没变,不能说明输出对。

分步演示

Golden Master
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;Report 程序与 master 比较脚本都是为实验自写的(没有使用 ApprovalTests 等库);每次运行都是新的 JVM,环境用 -Duser.language / -Duser.country / -Duser.timezone / -Dfile.encoding 切换
    • 没有验证:美国西部时区下 HashSet 顺序变化的原因(推测与身份哈希的生成序列被前面的初始化改变有关,未求证)、Windows 的换行符与路径分隔符、浮点格式化在不同 JDK 版本间的差异、ApprovalTests / Jest snapshot 等工具的 reporter 与 diff 工具、大文件 master 的存储与审阅成本
    京ICP备2026067502号-1京公网安备11010502063487号