Using Monkey Patching with gomonkey for Unit Testing in Go
This article demonstrates how to apply Monkey Patching in Go using the gomonkey library to unit‑test a simple HTTP service, covering code examples, dependency analysis, patch creation, test execution parameters, and practical considerations such as inlining and concurrency limitations.
In previous posts the author explained how to write testable Go code and how to refactor code for better testability. This article addresses the situation where legacy "bad" code cannot be easily tested and introduces Monkey Patching as a final solution.
Introduction
Monkey Patching, familiar to developers of dynamic languages like Python and JavaScript, can also be achieved in the static Go language. The author refers readers to related articles on Python monkey patches and a dedicated Go implementation.
HTTP Service Example
A minimal Go HTTP server is presented, exposing POST /users to create a user and GET /users/:id to retrieve a user. The source code is shown below:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int
Name string
}
func NewMySQLDB(host, port, user, pass, dbname string) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, dbname)
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
func NewUserHandler(store *gorm.DB) *UserHandler { return &UserHandler{store: store} }
type UserHandler struct { store *gorm.DB }
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
body, err := io.ReadAll(r.Body)
if err != nil { w.WriteHeader(http.StatusBadRequest); _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()); return }
defer func() { _ = r.Body.Close() }()
u := User{}
if err := json.Unmarshal(body, &u); err != nil { w.WriteHeader(http.StatusBadRequest); _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()); return }
if err := h.store.Create(&u).Error; err != nil { w.WriteHeader(http.StatusInternalServerError); _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()); return }
w.WriteHeader(http.StatusCreated)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
id := ps[0].Value
uid, _ := strconv.Atoi(id)
w.Header().Set("Content-Type", "application/json")
var u User
if err := h.store.First(&u, uid).Error; err != nil { w.WriteHeader(http.StatusInternalServerError); _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()); return }
_, _ = fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, u.ID, u.Name)
}
func setupRouter(handler *UserHandler) *httprouter.Router {
router := httprouter.New()
router.POST("/users", handler.CreateUser)
router.GET("/users/:id", handler.GetUser)
return router
}
func main() {
mysqlDB, _ := NewMySQLDB("localhost", "3306", "user", "password", "test")
handler := NewUserHandler(mysqlDB)
router := setupRouter(handler)
_ = http.ListenAndServe(":8000", router)
}The service listens on port 8000 and provides the two endpoints mentioned above.
Testing with Monkey Patching
The focus is on testing (*UserHandler).CreateUser . The method depends on the UserHandler.store field, which holds a *gorm.DB instance, and on several HTTP‑related parameters. Because the original code does not use interfaces for dependency injection, the author applies Monkey Patching to replace the Create method of the *gorm.DB object.
The gomonkey library is used. Install it with:
$ go get github.com/agiledragon/gomonkey/v2The test code is as follows:
func TestUserHandler_CreateUser(t *testing.T) {
mysqlDB := &gorm.DB{}
handler := NewUserHandler(mysqlDB)
router := setupRouter(handler)
// Apply monkey patch to replace mysqlDB.Create
patches := gomonkey.ApplyMethod(reflect.TypeOf(mysqlDB), "Create",
func(in *gorm.DB, value interface{}) (tx *gorm.DB) {
expected := &User{Name: "user1"}
actual := value.(*User)
assert.Equal(t, expected, actual)
return in
})
defer patches.Reset()
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/users", strings.NewReader(`{"name": "user1"}`))
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, "", w.Body.String())
}The test creates an empty *gorm.DB object (no real DB connection), patches its Create method, runs the HTTP request, and verifies the response code, header, and body.
Key points highlighted:
The patch is created with gomonkey.ApplyMethod and returns a *gomonkey.Patches object that must be reset after the test.
Running the test requires disabling Go's inlining optimization ( -gcflags=all=-l ) because gomonkey cannot handle inlined code.
Concurrency must be limited to a single test process ( -p 1 ) because gomonkey is not thread‑safe.
On ARM machines (e.g., Apple M2) the environment variable GOARCH=amd64 is needed to run the test.
Summary
The article presents Monkey Patching as a powerful technique to replace object behavior at runtime without modifying the original source, enabling unit tests for otherwise untestable legacy code. While gomonkey offers extensive patching capabilities, it has drawbacks: it does not support Go's inlining, is not concurrent‑safe, and has limited ARM support, so it should be used judiciously.
Full source code is available on GitHub, and readers are encouraged to explore additional gomonkey features such as patching functions, global variables, and function variables.
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.