Resolving MySQL Dependencies in Go Unit Tests with Fake and Mock Objects
This article explains how to write Go unit tests for HTTP handlers that depend on MySQL by using fake implementations and the gomock library to replace the store layer, demonstrating both fake object and mock approaches, code examples, and best practices for isolating external dependencies.
When writing unit tests for Go code that relies on external services such as a MySQL database, the external dependency must be replaced with a test double. This guide shows how to do that for an HTTP service that provides POST /users and GET /users/:id endpoints.
HTTP Service Example
package main
import (
"encoding/json"
"fmt"
"gorm.io/gorm"
"io"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
"github.com/jianghushinian/blog-go-example/test/mysql/store"
)
func NewUserHandler(db *gorm.DB) *UserHandler { return &UserHandler{store: store.NewUserStore(db)} }
type UserHandler struct { store store.UserStore }
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { /* ... */ }
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { /* ... */ }
func setupRouter(handler *UserHandler) *httprouter.Router { /* ... */ }
func main() {
mysqlDB, _ := store.NewMySQLDB("localhost", "3306", "user", "password", "test")
handler := NewUserHandler(mysqlDB)
router := setupRouter(handler)
_ = http.ListenAndServe(":8000", router)
}The UserHandler depends on the store.UserStore interface, which is defined in store/store.go and implemented by userStore that uses GORM to talk to MySQL.
Fake Testing
A simple way to break the MySQL dependency is to provide a fake implementation of store.UserStore :
type fakeUserStore struct{}
func (f *fakeUserStore) Create(user *store.User) error { return nil }
func (f *fakeUserStore) Get(id int) (*store.User, error) { return &store.User{ID: id, Name: "test"}, nil }Using the fake, the handler can be instantiated as:
handler := &UserHandler{store: &fakeUserStore{}}A unit test for GetUser then becomes a pure HTTP test without any database access.
Mock Testing with gomock
For larger projects a manually written fake becomes cumbersome. The gomock library can generate a mock automatically:
$ go get go.uber.org/mock/gomock@latest
$ go install go.uber.org/mock/mockgen@latest
$ mockgen -source store/store.go -destination store/mocks/gomock.go -package mocksThe generated MockUserStore implements the same interface. In a test you create a controller, set expectations, and inject the mock:
func TestUserHandler_GetUser_by_mock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserStore := mocks.NewMockUserStore(ctrl)
mockUserStore.EXPECT().Get(gomock.Eq(2)).Return(&store.User{ID: 2, Name: "user2"}, nil)
handler := &UserHandler{store: mockUserStore}
router := setupRouter(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/users/2", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"id":2,"name":"user2"}`, w.Body.String())
}The mock lets you verify that Get is called with the expected argument and to return different results for different scenarios, such as errors.
Additional gomock Features
gomock provides argument matchers ( Any() , Eq() , Not() , Nil() , Len() ), call count constraints ( Times() , MaxTimes() , AnyTimes() ) and ordering ( After() ), making it flexible for complex interfaces.
Summary
By defining the data‑access layer as an interface ( store.UserStore ) the HTTP handler can be tested without a real MySQL instance. A hand‑written fake is quick for simple cases, while gomock automates mock generation and provides powerful verification capabilities, enabling reliable unit tests for Go backend services.
Go Programming World
Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.