Bài trước, testcontainers cho ta test chống dịch vụ thật. Nhưng không phải lúc nào cũng nên — có khi bạn chỉ muốn test logic nghiệp vụ của một hàm mà không thực sự gọi database, dịch vụ thanh toán, hay API bên ngoài. Đó là lúc dùng mock: một cài đặt giả của interface phụ thuộc, nơi bạn khai trước hành vi mong đợi.

gomock (nay là go.uber.org/mock) là thư viện mock phổ biến nhất cho Go, và nó làm nhiều hơn "trả giá trị giả": nó verify — kiểm rằng code gọi đúng phương thức, đúng đối số, đúng số lần. Công cụ mockgen đi kèm sinh mock tự động từ interface, nên bạn không viết mock bằng tay. Bài này sinh mock thật, chạy test qua nó, và chứng minh gomock bắt được lời gọi sai — đồng thời nêu rõ khi nào mock đúng chỗ và khi nào nó phản tác dụng.

Interface phụ thuộc và nghiệp vụ dùng nó

type CongThanhToan interface {
	Tru(taiKhoan string, soTien int) error
}
func DatHang(ctg CongThanhToan, taiKhoan string, gia int) (string, error) {
	if gia <= 0 { return "", errors.New("giá không hợp lệ") }
	if err := ctg.Tru(taiKhoan, gia); err != nil {
		return "", err // lỗi thanh toán -> không tạo đơn
	}
	return "DON-OK", nil
}

DatHang phụ thuộc interface CongThanhToan. Để test logic của DatHang (kiểm giá, xử lý lỗi thanh toán) mà không gọi cổng thanh toán thật, ta mock CongThanhToan.

Sinh mock bằng mockgen

$ mockgen -source=code.go -destination=mock_code.go -package=mk

mockgen đọc interface và sinh ra một MockCongThanhToan đầy đủ: NewMockCongThanhToan(ctrl) để tạo, EXPECT() để khai kỳ vọng, và phương thức Tru được cài để ghi nhận lời gọi. Toàn bộ tự động — bạn không viết dòng mock nào bằng tay, và mock luôn khớp interface (đổi interface, sinh lại).

Test: khai kỳ vọng, gomock verify

ctrl := gomock.NewController(t)
m := NewMockCongThanhToan(ctrl)
// KỲ VỌNG: Tru("acc1",100) gọi ĐÚNG 1 lần, trả nil
m.EXPECT().Tru("acc1", 100).Return(nil).Times(1)

don, err := DatHang(m, "acc1", 100) // chạy nghiệp vụ với mock

EXPECT().Tru("acc1", 100).Return(nil).Times(1) khai một kỳ vọng đầy đủ: phương thức nào, đối số gì, trả về gì, gọi mấy lần. Khi test kết thúc, gomock tự động kiểm mọi kỳ vọng đã thỏa hay chưa — không cần bạn tự assert.

Ảnh chụp đoạn mã Go nền tối minh hoạ mocking với gomock và mockgen trong Go sinh mock tự động từ interface, vì sao mock cô lập đơn vị khỏi phụ thuộc muốn test nghiệp vụ DatHang mà không gọi dịch vụ thanh toán thật mock một cài đặt giả của interface bạn khai hành vi mong đợi gomock còn verify đúng lời gọi đúng đối số đúng số lần mockgen sinh mock tự động từ interface không viết tay, interface phụ thuộc và nghiệp vụ dùng nó type CongThanhToan interface Tru taiKhoan string soTien int error func DatHang ctg CongThanhToan taiKhoan string gia int string error if gia nhỏ hơn bằng 0 return giá không hợp lệ if err bằng ctg Tru taiKhoan gia err khác nil return err lỗi thanh toán không tạo đơn return DON-OK nil, sinh mock mockgen source mockgen source code.go destination mock_code.go package mk sinh MockCongThanhToan cộng EXPECT cộng Tru tự động từ interface func NewMockCongThanhToan ctrl gomock Controller trỏ MockCongThanhToan func m MockCongThanhToan EXPECT trỏ MockCongThanhToanMockRecorder, test khai kỳ vọng gomock verify ctrl bằng gomock NewController t m bằng NewMockCongThanhToan ctrl kỳ vọng Tru acc1 100 gọi đúng 1 lần trả nil m EXPECT Tru acc1 100 Return nil Times 1 don err bằng DatHang m acc1 100 chạy nghiệp vụ với mock khi test kết thúc gomock tự kiểm mọi kỳ vọng đã thỏa hay chưa

Hình 1: gomock + mockgen — mockgen sinh MockCongThanhToan từ interface; test khai kỳ vọng bằng EXPECT().Tru(...).Return(...).Times(...) rồi chạy nghiệp vụ với mock, gomock tự verify khi test kết thúc.

Đo thật: ba test PASS, và gomock bắt lời gọi sai

Ba test qua mock đều PASS:

=== RUN   TestDatHang_ThanhCong
--- PASS   // Tru("acc1",100) gọi đúng 1 lần -> DON-OK
=== RUN   TestDatHang_KhongDu
--- PASS   // mock Tru trả ErrKhongDu -> DatHang không tạo đơn
=== RUN   TestDatHang_GiaSai
--- PASS   // giá âm -> Tru KHÔNG được gọi (gomock tự verify)
ok  example.com/mk  0.001s

Test thứ ba đáng chú ý: khi giá âm, DatHang trả lỗi trước khi gọi Tru. Test không đặt EXPECT nào — và nếu code lỡ gọi Tru, gomock sẽ báo lỗi "unexpected call". Nó PASS nghĩa là Tru đúng là không được gọi. gomock verify cả hai chiều: kỳ vọng phải xảy ra, và cái không kỳ vọng thì không được xảy ra.

Để chứng minh sức mạnh verify, tôi viết một test cố tình sai — kỳ vọng Tru("acc1", 200) nhưng code thực gọi Tru("acc1", 100):

--- FAIL: TestGomock_BatSaiDoiSo
code.go:18: Unexpected call to Tru([acc1 100]) because:
    expected call doesn't match the argument at index 1
controller.go:98: missing call(s) to Tru(acc1, 200)

gomock so đối số: 100 != 200 ở vị trí index 1, nên nó báo "doesn't match the argument at index 1", và vì kỳ vọng Tru(acc1, 200) chưa bao giờ được gọi, nó báo "missing call(s)". Test FAIL. Đây là điều phân biệt mock (kiểm tương tác) với stub thường (chỉ trả giá trị): mock ràng buộc code phải gọi đúng cái đã khai.

Ảnh chụp bảng kết quả đo thật nền tối mocking gomock mockgen trong Go chạy bằng go test Go 1.23 arm64 go.uber.org mock v0.4 mockgen sinh mock, ba test với mock PASS RUN TestDatHang_ThanhCong PASS Tru acc1 100 gọi đúng 1 lần DON-OK RUN TestDatHang_KhongDu PASS mock Tru trả ErrKhongDu DatHang không tạo đơn RUN TestDatHang_GiaSai PASS giá âm Tru không được gọi gomock tự verify ok example.com mk 0.001s, gomock VERIFY bắt lời gọi sai đối số test cố tình kỳ vọng Tru acc1 200 nhưng code gọi Tru acc1 100 FAIL TestGomock_BatSaiDoiSo code.go dòng 18 Unexpected call to Tru acc1 100 because expected call doesn't match the argument at index 1 controller.go dòng 98 missing call s to Tru acc1 200 gomock so đối số 100 khác 200 báo doesn't match argument index 1 và kỳ vọng chưa được gọi missing call s test FAIL, gomock kiểm ba thứ tự động đúng lời gọi phương thức nào được gọi Tru đúng đối số đối số khớp acc1 100 lệch là FAIL đúng số lần Times 1 thiếu gọi hoặc gọi dư đều FAIL, cốt lõi mock cài đặt giả của interface để cô lập đơn vị khỏi phụ thuộc mockgen sinh mock tự động từ interface không viết tay EXPECT khai kỳ vọng lời gọi đối số số lần giá trị trả về verify gomock tự kiểm mọi kỳ vọng khi test kết thúc lệch là FAIL đo thật 3 test PASS kỳ vọng sai đối số bị bắt 100 khác 200 đánh đổi mock che khác biệt với dịch vụ thật bài testcontainers test ràng buộc cài đặt dễ vỡ khi refactor đừng lạm dụng

Hình 2: Đo thật — 3 test qua mock (thành công, lỗi thanh toán, giá sai) đều PASS; gomock verify bắt lời gọi sai đối số (kỳ vọng Tru(acc1,200) mà code gọi Tru(acc1,100) → "doesn't match argument at index 1" + "missing call(s)" → FAIL); kiểm đúng lời gọi/đối số/số lần tự động.

Đánh đổi cần cân nhắc

Mock che giấu khác biệt với dịch vụ thật. Đây là đánh đổi lớn nhất, nối thẳng với bài testcontainers trước. Mock trả về đúng cái bạn khai — nên nếu giả định của bạn về dịch vụ thật sai (cú pháp SQL, kiểu dữ liệu, hành vi lỗi), test qua mock vẫn xanh trong khi production hỏng. Mock tốt cho test logic nghiệp vụ (nhánh điều kiện, xử lý lỗi), nhưng không thay được integration test cho các đường ranh giới với dịch vụ thật. Dùng cả hai: mock cho logic, testcontainers/integration cho ranh giới.

Mock ràng buộc cài đặt, dễ vỡ khi refactor. Vì gomock kiểm chính xác phương thức nào được gọi với đối số gì, test mock gắn chặt với cách code làm việc, không chỉ kết quả. Refactor nội bộ (đổi thứ tự gọi, gộp lời gọi) có thể làm test mock FAIL dù hành vi bên ngoài không đổi — test giòn (brittle). Chỉ mock ở ranh giới thật sự cần cô lập (I/O, dịch vụ ngoài), đừng mock mọi thứ; over-mocking tạo test kiểm cài đặt thay vì hành vi.

Chỉ mock được qua interface. gomock/mockgen chỉ sinh mock cho interface, không cho struct cụ thể. Điều này thúc đẩy thiết kế tốt (phụ thuộc vào interface, không vào cài đặt cụ thể) — nhưng nếu code bạn phụ thuộc thẳng struct hay hàm cụ thể, phải refactor đưa vào interface trước khi mock được. Đây thường là thay đổi tốt, nhưng là chi phí phải tính.

Ba ý mang về

  1. gomock + mockgen cô lập đơn vị khỏi phụ thuộc: mockgen sinh mock tự động từ interface (không viết tay), test khai kỳ vọng bằng EXPECT().Method(args).Return(...).Times(n) — đo thật, 3 test qua mock cho logic DatHang (thành công, lỗi thanh toán, giá sai) đều PASS.
  2. gomock verify cả ba chiều tự động: đúng phương thức, đúng đối số, đúng số lần — đo thật, kỳ vọng Tru(acc1, 200) mà code gọi Tru(acc1, 100) bị bắt ngay ("doesn't match argument at index 1" + "missing call(s)"), phân biệt mock (kiểm tương tác) với stub (chỉ trả giá trị).
  3. Nêu rõ đánh đổi: mock che giấu khác biệt với dịch vụ thật (cần integration/testcontainers cho ranh giới), ràng buộc cài đặt nên dễ vỡ khi refactor (đừng over-mock), và chỉ mock được qua interface (thúc đẩy thiết kế tốt nhưng có chi phí refactor).

Phần sau ta xét một kỹ thuật test cho đầu ra lớn/phức tạp: golden file testing trong Go — lưu đầu ra kỳ vọng vào tệp "vàng", so sánh tự động, và cập nhật bằng cờ khi hành vi đổi có chủ đích.