Backend Development 12 min read

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.

Go Programming World
Go Programming World
Go Programming World
Using Monkey Patching with gomonkey for Unit Testing in Go

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/v2

The 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.

BackendGoUnit Testinghttpmonkey-patchinggomonkey
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.