Có một loại hàm mà viết test kiểu want := "..." trở nên khổ sở: hàm sinh đầu ra lớn — một báo cáo văn bản 30 dòng, một khối HTML, một JSON phức tạp, mã nguồn sinh tự động, hay output của một lệnh CLI. Nhét cả đầu ra kỳ vọng thành một chuỗi trong file test khiến test rối, khó đọc, và cực khổ để cập nhật khi định dạng đổi.
Golden file testing (còn gọi snapshot testing) giải gọn: lưu đầu ra kỳ vọng vào một tệp riêng (tệp "vàng"), và test chỉ việc so đầu ra thật với nội dung tệp đó. Khi hành vi đổi có chủ đích, bạn chạy go test -update để ghi lại tệp thay vì sửa chuỗi bằng tay. Bài này dựng golden file testing thật trong Go, đo trọn vòng đời của nó — sinh, so, bắt khác biệt, và cập nhật — cùng những cạm bẫy.
Cờ -update để ghi lại golden file
Mấu chốt là một cờ dòng lệnh phân biệt hai chế độ:
var update = flag.Bool("update", false, "ghi lại golden file")
// go test -update -> sinh/cập nhật tệp vàng
// go test -> so đầu ra với tệp vàng (không ghi)
Test: -update thì ghi, không thì so
got := XuatHoaDon(h) // đầu ra thật
golden := filepath.Join("testdata", "hoadon.golden")
if *update { // chế độ ghi lại
os.WriteFile(golden, []byte(got), 0644)
return
}
want, err := os.ReadFile(golden) // đọc tệp vàng
if err != nil {
t.Fatalf("chưa có golden, chạy: go test -update")
}
if got != string(want) { // so từng byte
t.Errorf("đầu ra KHÁC golden:\n--- GOT ---\n%s\n--- WANT ---\n%s", got, want)
}
Logic đơn giản: nếu -update bật thì ghi đầu ra hiện tại vào tệp và xong; nếu không thì đọc tệp và so. Dùng thư mục testdata/ — Go tự bỏ qua thư mục này khi build, nên đây là nơi chuẩn để đặt dữ liệu test.

Hình 1: Golden file testing — cờ -update phân biệt chế độ ghi (sinh/cập nhật tệp vàng) và chế độ so (đọc tệp, so từng byte với đầu ra thật); dùng thư mục testdata/ mà Go tự bỏ qua khi build.
Đo thật: trọn vòng đời golden file
Bước 1 — lần đầu chưa có golden:
$ go test
--- FAIL: chưa có golden file, chạy: go test -update
$ go test -update
PASS ok example.com/golden
Lần đầu test FAIL với hướng dẫn rõ ràng; chạy -update sinh ra testdata/hoadon.golden (commit vào repo). Nội dung tệp vàng chính là đầu ra kỳ vọng:
HOA DON - Khach: Nguyen Van A
================================
Banh mi 25000
Ca phe 45000
Tra da 5000
--------------------------------
TONG CONG 75000
Bước 2 — chạy lại bình thường: đầu ra khớp từng byte với tệp vàng → PASS.
Bước 3 — đổi code (ví dụ đổi tiêu đề "HOA DON" → "HOA DON BAN HANG"): golden test bắt khác biệt ngay, kèm diff chỉ rõ chỗ khác:
$ go test # sau khi đổi tiêu đề trong code
--- FAIL: TestXuatHoaDon_Golden
--- GOT ---
HOA DON BAN HANG - Khach: Nguyen Van A
--- WANT ---
HOA DON - Khach: Nguyen Van A
So từng byte nên nó chỉ ra chính xác dòng khác. Đây là lưới an toàn: bất kỳ thay đổi nào tới đầu ra — dù vô tình — đều bị bắt.
Bước 4 — nếu đổi là có chủ đích: chạy -update để chấp nhận đầu ra mới:
$ go test -update
PASS
$ head -1 testdata/hoadon.golden
HOA DON BAN HANG - Khach: Nguyen Van A
Golden file cập nhật. Điểm hay: thay đổi này hiện ra dưới dạng diff của tệp golden khi review commit — người review thấy rõ đầu ra đã đổi thế nào và quyết định có đúng không. Đầu ra không bao giờ đổi ngầm.

Hình 2: Đo thật trọn vòng đời — -update sinh golden (PASS), chạy lại so PASS, đổi tiêu đề trong code thì test FAIL kèm diff GOT/WANT chỉ rõ chỗ khác, và -update chấp nhận thay đổi có chủ đích (golden cập nhật thành HOA DON BAN HANG).
Đánh đổi cần cân nhắc
Dễ -update một cách mù quáng — cạm bẫy lớn nhất. Sức mạnh của golden file (cập nhật dễ) cũng là điểm yếu: khi test FAIL, phản xạ lười là chạy -update ngay để test xanh, mà không đọc diff. Làm vậy là biến lưới an toàn thành vô dụng — bạn "chấp nhận" cả bug. Kỷ luật bắt buộc: luôn đọc diff golden trong code review trước khi merge, coi nó như một phần thay đổi thật sự. Golden file chỉ an toàn khi diff được xem xét nghiêm túc.
Đầu ra không tất định làm golden vỡ. Golden file so chính xác từng byte, nên đầu ra chứa yếu tố thay đổi mỗi lần chạy — thời gian hiện tại, số ngẫu nhiên, thứ tự map (Go lặp map ngẫu nhiên!), địa chỉ con trỏ — sẽ khiến test FAIL sai. Phải làm đầu ra tất định trước khi golden hóa: sắp thứ tự (như tôi sort.Strings các mặt hàng trong ví dụ), tiêm thời gian cố định, seed random cố định. Nếu không kiểm soát được, golden file không phù hợp.
Golden nhị phân hoặc quá lớn khó review. Golden file văn bản thì diff đọc được; nhưng golden là ảnh, PDF, hay khối nhị phân thì diff vô nghĩa với người — bạn không thấy cái gì đổi, chỉ thấy "khác". Với đầu ra như vậy, cân nhắc so ở mức có nghĩa hơn (kích thước, checksum, vài thuộc tính) thay vì byte thô, hoặc dùng công cụ diff chuyên cho định dạng đó. Golden file mạnh nhất cho đầu ra văn bản mà con người đọc được diff.
Ba ý mang về
- Golden file testing lưu đầu ra kỳ vọng vào tệp và so tự động — hợp cho đầu ra lớn (báo cáo, HTML, JSON, mã sinh, output CLI) mà viết assertion tay thì rối: đo thật trọn vòng đời, sinh bằng
-update, so PASS, đổi code thì FAIL kèm diff GOT/WANT. - Cờ
-updatelà cơ chế cập nhật có kiểm soát: đổi có chủ đích thì ghi lại tệp thay vì sửa chuỗi tay, và thay đổi hiện ra dưới dạng diff golden khi review commit — đầu ra không bao giờ đổi ngầm; dùngtestdata/mà Go tự bỏ qua khi build. - Nêu rõ cạm bẫy: dễ
-updatemù không đọc diff (biến lưới an toàn thành vô dụng — phải review diff nghiêm túc), đầu ra không tất định (thời gian, random, thứ tự map) làm golden vỡ nên phải làm tất định trước, và golden nhị phân/quá lớn khó review.
Phần sau ta quay lại đo hiệu năng cho chính xác: benchstat trong Go — chạy benchmark nhiều lần, tính trung bình và độ lệch, và so hai phiên bản có ý nghĩa thống kê thay vì so một con số đơn lẻ dễ nhiễu.