Go có testing trong thư viện chuẩn, và nó không có assertEquals, không có @Test, không có mock framework. Bài này về cách viết test kiểu Go.

Test đầu tiên

// tệp tien_test.go, cùng package
func TestDinhDang(t *testing.T) {
	if got := DinhDang(100); got != "1,00" {
		t.Errorf("DinhDang(100) = %q, mong %q", got, "1,00")
	}
}

Quy ước: tệp *_test.go, hàm TestXxx(t *testing.T). Không cần chú thích, không cần đăng ký.

t.Errorf báo hỏng nhưng chạy tiếp; t.Fatalf dừng ngay test đó. Dùng Fatal khi các bước sau phụ thuộc bước này.

Không có assertcó chủ đích. Nhóm Go cho rằng if cộng thông điệp rõ ràng dễ đọc hơn một tầng trừu tượng, và nó buộc bạn viết thông điệp lỗi có ích.

Thông điệp nên theo mẫu: got, want, và đầu vào. Khi CI đỏ lúc 6 giờ chiều thứ Sáu, đó là thứ bạn cần.

Bảng test: mẫu chuẩn của Go

cases := []struct {
	ten  string
	xu   int64
	mong string
}{
	{"không", 0, "0,00"},
	{"một xu", 1, "0,01"},
	{"âm", -150, "-1,50"},
}
for _, c := range cases {
	t.Run(c.ten, func(t *testing.T) {
		if got := DinhDang(c.xu); got != c.mong {
			t.Errorf("DinhDang(%d) = %q, mong %q", c.xu, got, c.mong)
		}
	})
}
  === RUN   TestDinhDang/không
  === RUN   TestDinhDang/một_xu
  === RUN   TestDinhDang/âm
  --- PASS: TestDinhDang (0.00s)
      --- PASS: TestDinhDang/một_xu (0.00s)

t.Run tạo subtest có tên riêng, chạy độc lập, và hỏng một cái không che các cái khác. Đây là tương đương @ParameterizedTest của JUnit, nhưng không cần chú thích nào.

Chạy riêng một trường hợp:

go test -run 'TestDinhDang/âm'

Thêm trường hợp mới là thêm một dòng vào bảng — đó là lý do mẫu này phổ biến tới mức gần như bắt buộc trong mã Go.

t.Parallel

t.Run(n, func(t *testing.T) {
	t.Parallel()
	...
})

Subtest gọi t.Parallel() sẽ tạm dừng, và chạy song song với các subtest song song khác sau khi hàm cha xong.

Rất đáng cho test có I/O. Nhưng nhớ: chúng dùng chung mọi trạng thái toàn cục, nên phải chạy được độc lập — và nên chạy với -race.

Tổ chức

package tien (cùng package) — test được cả hàm viết thường. Đây là mặc định.

package tien_test (khác package) — chỉ thấy API công khai. Dùng khi muốn kiểm rằng API của bạn đủ dùng từ bên ngoài, và tránh phụ thuộc vòng.

Hai kiểu này cùng tồn tại trong một thư mục được — Go cho phép đúng ngoại lệ này.

TestMain để dựng và dọn cho cả gói:

func TestMain(m *testing.M) {
	dungCSDL()
	ma := m.Run()
	donDep()
	os.Exit(ma)
}

t.Cleanup để dọn theo từng test, chạy kể cả khi test hỏng:

t.Cleanup(func() { os.Remove(tep) })

t.TempDir cho thư mục tạm tự dọn — gọn hơn os.MkdirTemp cộng defer.

Độ phủ

go test -cover
go test -coverprofile=c.out && go tool cover -html=c.out
  coverage: 18.8% of statements

Lệnh thứ hai mở trình duyệt với mã được tô màu — dòng nào chưa chạy hiện đỏ. Đây là cách hữu ích nhất để dùng độ phủ: tìm nhánh chưa test, không phải để đạt một con số.

Đừng đặt mục tiêu phần trăm. Độ phủ 100% vẫn có thể không kiểm gì cả nếu test không có assertion đúng.

Fuzzing: có sẵn từ Go 1.18

func FuzzDinhDang(f *testing.F) {
	f.Add(int64(0))
	f.Add(int64(-1))
	f.Fuzz(func(t *testing.T, xu int64) {
		if DinhDang(xu) == "" { t.Fatal("chuỗi rỗng") }
	})
}
  go test -fuzz=FuzzDinhDang -fuzztime=5s
  fuzz: elapsed: 5s, execs: 4846391 (916029/sec), new interesting: 1

4,8 triệu lần chạy trong 5 giây, với đầu vào do Go tự sinh và tự tiến hoá theo độ phủ mã.

Khi tìm được đầu vào làm hỏng, Go lưu nó vào testdata/fuzz/ và biến thành test thường — nên nó không bao giờ quay lại.

Rất hợp cho hàm phân tích dữ liệu, giải mã, xử lý chuỗi. Đây là thứ Java không có trong thư viện chuẩn.

Test HTTP: httptest

srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/don/1")

Hoặc gọi handler trực tiếp, không qua mạng:

w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/don/1", nil)
mux.ServeHTTP(w, r)
if w.Code != 200 { t.Errorf("mã = %d", w.Code) }

Cách thứ hai nhanh hơn nhiều và đủ cho phần lớn test handler.

Có nên dùng thư viện

testify rất phổ biến (assert.Equal, require.NoError, mock). Nó làm test ngắn hơn.

Nhưng cộng đồng Go chia rẽ về nó, và lập luận phản đối có lý: thư viện chuẩn đã đủ, và if tường minh làm rõ chính xác điều gì được kiểm.

Lời khuyên của tôi: bắt đầu bằng thư viện chuẩn. Với so sánh struct phức tạp thì google/go-cmp đáng thêm — nó in ra khác biệt thay vì chỉ "không bằng nhau":

if d := cmp.Diff(mong, got); d != "" {
	t.Errorf("khác biệt (-mong +got):\n%s", d)
}

Thử ba mươi giây

go test -race -count=5 ./...

-count=5 chạy mỗi test năm lần, tăng cơ hội trúng đường mã hiếm; -race bắt lỗi đua như bài 36.

Nếu bộ test của bạn xanh với lệnh này, nó đáng tin hơn hẳn so với go test ./... trần.

Ngày mai: benchmark và pprof — đo hiệu năng cho đúng.