← 中文资源

简体中文版本

黄金主测试

黄金主测试把遗留代码当前行为记录为参考,再比较后续输出,常用于在重构未测代码前先固化其行为。

阅读英文原文

概念概述

黄金主测试(Golden Master Testing)指的是:黄金主测试把遗留代码当前行为记录为参考,再比较后续输出,常用于在重构未测代码前先固化其行为。

从更严格的定义看,Golden master testing is the practice of recording a system’s current outputs for a range of inputs and using that recording as the expected result for subsequent runs, especially before changing untested code. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It is applied when refactoring a legacy module whose intended behaviour is not documented and only the existing output is known. 它与 Test Flakiness Metrics、Accessibility Tree Testing、The Test Oracle Problem 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • a golden master asserts the behaviour is correct
  • it makes further change unnecessary

需要避免的反模式

  • generating the baseline from the untested code without reviewing samples
  • keeping an enormous golden file no one inspects

实践要点

  1. The technique pins down observable behaviour so a refactor can be proven behaviour-preserving even where no specification exists.
  2. It inherits any existing bugs, so the mastered behaviour is a snapshot, not a statement of correctness; each sample should still be reviewed.
  3. A large brittle baseline is hard to maintain, so scope the recorded inputs to representative cases and keep the file reviewable.

参考来源

https://en.wikipedia.org/wiki/Characterization_test

代码示例

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)
	}
}