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.

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.

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ề
- gomock + mockgen cô lập đơn vị khỏi phụ thuộc:
mockgensinh mock tự động từ interface (không viết tay), test khai kỳ vọng bằngEXPECT().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. - 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ọiTru(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ị). - 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.