17.1 单元测试、Golden 测试、快照测试与集成测试
编译器测试不能只回答“进程是否成功退出”。编译器由一连串契约组成:词法分析器必须保留正确的 token 边界,解析器要构造符合语法含义的树,名字解析要绑定到正确声明,优化必须保持可观察行为,代码生成还要遵循目标 ABI。优秀的测试套件会在多个边界放置检查点,使错误既能被发现,也能迅速定位。
单元测试隔离一个小而确定的组件。例如,扫描器测试把源码映射为 token 种类和字节区间;IR verifier 测试输入一个损坏的基本块,并期待精确的不变量错误。单元测试速度快、定位清楚,但大量 mock 可能掩盖真实阶段之间的不一致。应优先使用真实值对象和小 fixture,而不是复杂 mock 网络。对纯变换还可以加入性质测试:格式化后再解析应保留结构,alpha-renaming 不应改变行为,每个分支目标都必须真实存在。
集成测试跨越真实边界。它可以编译源码、汇编并链接、在受控环境中执行程序,再比较输出和退出码。它能发现孤立测试看不到的问题,例如 debug location 记录的是字节偏移,而渲染器却按显示列解释。集成测试代价较高、失败面也更大,因此必须保留中间产物与各阶段日志。
Golden 文件是需要审查的契约,不是自动生成的真理
Golden 测试保存一份可信输出,例如 token、AST 文本、诊断、汇编或目标文件元数据,然后与新运行结果比较。快照测试本质相近,只是通常由测试框架管理并方便更新。当输出很丰富、手写断言容易漏字段时,它们特别有效:一个诊断 fixture 就能同时固定严重级别、错误码、源码范围、notes 与 fix-its。
真正的危险是习惯性点击“更新全部快照”。如果用被测编译器重新生成期望结果,就可能把造成变化的 bug 一起批准。Golden 差异至少要分类:语义变化改变含义或执行行为;表示变化只影响无害格式;非确定性变化暴露不稳定的遍历次序、路径、地址、locale 或时间戳。审查者应先理解类别,再接受新基线。
只规范化明确不重要的可变字段:把临时根目录替换为 <TMP>,对没有顺序契约的集合排序,并固定 locale 与 target。不能仅仅为了让失败的优化测试变绿就删除指令顺序。很多时候,比较结构化表示比比较脆弱文本更可靠:解析 JSON 诊断后断言字段;或者反汇编 object,忽略 section 地址但保留 relocation。
用不同失效模式组成测试组合
端到端测试能证明产品整体可用,却不应取代窄测试。平衡的组合通常包括大量便宜的单元测试与不变量检查、覆盖丰富界面的少量 golden 测试、针对高风险接缝的组件集成,以及较小的跨平台端到端矩阵。每个已修复 bug 都应加入最小 reproducer 和预期失效模式,而不是只复制整个用户工程。
Mutation testing 可以审计防线:故意反转条件、删除优化前置条件或破坏 relocation kind。如果没有测试失败,说明套件并未保护该行为。负面测试同样重要;非法输入应产生正确诊断,不能崩溃或悄悄输出代码。
Fixture 必须 hermetic:固定工具版本,清理继承的环境变量,控制时钟和随机种子,隔离文件系统。失败记录要足以复现,包括 compiler build ID、flags、target triple、依赖版本和 seed。并行分片应保持确定性;flaky 测试可以暂时隔离,但必须有负责人和到期日,不能靠无限重试隐藏。目标不是堆出最大的测试数量,而是用快速、可审查且相互独立的证据覆盖真实风险。