功能说明
格式化输出、代码生成、报表渲染这类函数的正确性很难逐字段断言,黄金文件测试把一次人工确认过的输出保存到仓库,后续运行只要与文件一致就通过。它覆盖了完整输出,能发现字段顺序变化、空格差异和多行结构问题。
示例提供写入与比较两个函数:写入会自动创建目录,比较会先去掉整体首尾空白再逐字节对比,不一致时生成只包含差异行的报告,并额外提示行数变化,便于快速定位问题。
关键代码
WriteGolden 使用 os.MkdirAll 保证父目录存在,再以固定权限写入。CompareGolden 读取期望文件,用 bytes.Equal 比较去除首尾空白后的内容,只有不一致才调用 diffLines。diffLines 按行切分,取两侧较短长度逐行比较,只输出不同的行,前面标减号表示期望值、加号表示实际值;行数不同则追加提示。main 在临时目录中先写后比,再故意改一个数字触发差异。
使用注意
黄金文件必须随代码一起提交,否则测试在全新环境会失败。更新黄金文件应有显式开关或单独命令,不能让普通测试运行自动覆盖,否则会掩盖回归。文件行尾应统一,Windows 与 Unix 混用会造成假失败。大文件差异输出要限量,避免日志爆炸。内容包含时间戳、随机数或绝对路径时需要先归一化。
验证方式
第一次比较输出 match: ok,说明写入与读取一致。第二次把用户数改成四,比较返回错误,输出包含减号行 users=3 和加号行 users=4,并提示黄金文件名。可以再删除一行,确认行数提示出现,验证差异函数对长度变化也有效。
代码示例
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
)
func WriteGolden(target string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return fmt.Errorf("create golden dir: %w", err)
}
return os.WriteFile(target, data, 0o644)
}
func diffLines(want, got string) string {
wantLines := strings.Split(strings.TrimSpace(want), "\n")
gotLines := strings.Split(strings.TrimSpace(got), "\n")
limit := len(wantLines)
if len(gotLines) < limit {
limit = len(gotLines)
}
var builder strings.Builder
for i := 0; i < limit; i++ {
if wantLines[i] != gotLines[i] {
fmt.Fprintf(&builder, "- %s\n+ %s\n", wantLines[i], gotLines[i])
}
}
if len(wantLines) != len(gotLines) {
fmt.Fprintf(&builder, "line count: want %d got %d\n", len(wantLines), len(gotLines))
}
return builder.String()
}
func CompareGolden(target string, got []byte) error {
want, err := os.ReadFile(target)
if err != nil {
return fmt.Errorf("read golden: %w", err)
}
if !bytes.Equal(bytes.TrimSpace(got), bytes.TrimSpace(want)) {
return fmt.Errorf("golden mismatch for %s\n%s", filepath.Base(target), diffLines(string(want), string(got)))
}
return nil
}
func main() {
dir, err := os.MkdirTemp("", "golden-*")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
target := filepath.Join(dir, "report.golden")
report := []byte("users=3\norders=5\n")
if err := WriteGolden(target, report); err != nil {
panic(err)
}
if err := CompareGolden(target, report); err != nil {
fmt.Println("unexpected mismatch:", err)
} else {
fmt.Println("match: ok")
}
if err := CompareGolden(target, []byte("users=4\norders=5\n")); err != nil {
fmt.Println("mismatch detected:")
fmt.Println(err)
}
}