In Go development, Unit Test is inevitable. And it is essential to use Mock when writing Unit Tests.
- Mock can help test isolate the business logic it depends on, enabling it to compile, link, and run independently.
- Mock needs Stub. Stub function replaces the real business logic function, returns the required result, and assists the test.
I involved the related test code for Controllers while writing Kubernetes Operator recently, and there would be mocks for GRPC and HTTP requests. I did it in an old fashion way, but I believe there is a better and more graceful way to handle this.
Classify the various scenarios that require mock into the following categories.
- Global variable
- Function
- Interface
And when it comes to details, there will be mocks such as GRPC methods, HTTP requests, etc. However, the appearances may vary, the essence remains unchanged. As long as you master the core theory, it easily helps writing the unit tests.
Common Mock frameworks in Golang.
- GoMock¹
- Go httptest
- GoStub²
- GoMonkey³
- Testifyâ´
Generally speaking, the straightest method is to implement Mock by defining an interface, which you can use with the other frameworks of GoStub, GoMock, and GoMonkey in pairs or groups.
Testify is a relatively more comprehensive test framework with separate Mock support. You can use it either with your own framework or with the Mock frameworks listed above.
Go httptest, is the official GoLang framework for Mock HTTP requests.
Now I will analyze the Mock support of each framework from several perspectives.
- Mock a global variable
- Mock an interface
- Mock a function, with return value or without a return value.
GoMock
Using an interface to mock is the most common in various Golang codes without any dependencies. As I mentioned at the beginning of the article, I implement mock in the following ways to encapsulate HTTP and GRPC requests.
- Abstract method to interface
- Define Mock’s object and implement the mock function
- initialize the Mock object, override the real logic in the unit test
But this manual implementation of interface mock is relatively mechanical. Golang officially provides GoMock to help developers lift efficiency and better regulate Mock behavior.
GoMock was released in 2011 as part of the official version of Go. It implements a relatively complete interface-based Mock function, can be well integrated with Golang’s built-in testing package, and can also be used in other test environments. GoMock testing framework includes:
- GoMock (
github.com/golang/mock/gomock
) package completes the management of the life cycle of Mock objects. - The mockgen(
github.com/golang/mock/mockgen
) tool generates the Mock class source file corresponding to the interface.
Define an interface
Let’s say we want to Mock an interface. Design a Data interface for data operations, save the information of the key-value structure and value is an array of []byte
.
// CRUD
type Data interface {
Send(key string, value []byte) error
Get(key string) ([]byte, error)
}
Generate Mock Stub file
The next step is to generate the Stub file after clarifying the Mock interface. There are two operation modes when using the mockgen tool, source file, and reflection.
- The source file mode generates a mock class file through a file containing interface definitions, which takes effect through the -source flag.
mockgen -source=data.go
- The reflection mode generates a mock class file by building a program and understanding the interface with reflection, which takes effect through two non-flag parameters: the import path and a comma-separated list of symbols (optional multiple interfaces)
mockgen abc/data Data,Message
The generated mock_data.go
file,
// Automatically generated by MockGen. DO NOT EDIT!
// Source: infra/db (interfaces: Date)
package mock_db
import (
gomock "github.com/golang/mock/gomock"
)
// MockData is a mock of Data interface
type MockData struct {
ctrl *gomock.Controller
recorder *MockDataMockRecorder
}
// MockDataMockRecorder is the mock recorder for MockData
type MockDataMockRecorder struct {
mock *MockData
}
// NewMockData creates a new mock instance
func NewMockData(ctrl *gomock.Controller) *MockData {
mock := &MockData{ctrl: ctrl}
mock.recorder = &MockDataMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (_m *MockData) EXPECT() *MockDataMockRecorder {
return _m.recorder
}
// Create mocks base method
func (_m *MockData) Send(_param0 string, _param1 []byte) error {
ret := _m.ctrl.Call(_m, "Create", _param0, _param1)
ret0, _ := ret[0].(error)
return ret0
}
Use mock objects to test
- Import mock related packages
import (
"testing"
. "github.com/golang/mock/gomock"
"test/mock/db"
...
)
- Mock controller
The mock controller is generated through the NewController interface. It is the call control of the application layer and defines the scope and life cycle of mock objects and their expectations.
It is safe for multiple coroutines to call the controller simultaneously. When the call is over, the controller checks whether all remaining expected calls meet the conditions.
The controller code is as follows.
ctrl := NewController(t)
defer ctrl.Finish()
You need to inject the controller when creating the mock object. If there are multiple mock objects, then inject the same controller, as shown below.
ctrl := NewController(t)
defer ctrl.Finish()
mock := mock_db.NewMockData(ctrl)
mockGrpc := mock_api.NewGrpcMethod(ctrl)
- Injection of mock functions
The controller maintains mock objects via the map, with one behavior corresponds to one item of the map.
The map’s value type is a slice because a method may be called multiple times in a use case. The controller will add a mock result when the mock object performs behavior injection, while the controller will remove the item after it is called.
Suppose there is a scenario: You fail in getting the object first, but you succeed in creating the object later. After then, you will succeed when you attempt to get the domain object again. The behavior injection code of the mock object corresponding to this scene is as follows:
mockData.EXPECT().Get(Any()).Return(nil, ErrAny)
mockData.EXPECT().Create(Any(), Any()).Return(nil)
mockData.EXPECT().Get(Any()).Return(byte1, nil)
bytes1 is the serialized result of the domain object, such as,
obj := Data{...}
bytes1, err := json.Marshal(obj)
...
When creating objects in batches, you can use the Times keyword:
mockData.EXPECT().Create(Any(), Any()).Return(nil).Times(5)
httptest
httptest is Golang’s own standard mock server dependency library. Through it, developers can simulate handlers and HTTP server.
Below is a simple example of using it.
func GetInfo(api string) ([]User, error) {
url := fmt.Sprintf("%s/getlist?name=%s", api, NAME)
resp, _ := http.Get(url) // the http call need to Mock
bodybytes, _ := ioutil.ReadAll(resp.Body)
users := make([]User, 0)
_ = json.Unmarshal(bodybytes, &users)
return users, nil
}
// test
func TestGetInfoOK(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(StudentRespBytes)
if r.Method != "GET" {
t.Errorf("Except 'Get' got '%s'", r.Method)
}
if r.URL.EscapedPath() != "/getlist" {
t.Errorf("wrong path'%s'", r.URL.EscapedPath())
}
r.ParseForm()
name := r.Form.Get("name")
if name!= "abc" {
t.Errorf("Except rquest to have 'name=abc',got '%s'", name)
}
}))
defer ts.Close()
api := ts.URL
fmt.Printf("Url:%s\n", api)
resp, err := GetInfo(api)
if err != nil {
fmt.Println("ERR:", err)
} else {
fmt.Println("resp:", resp)
}
}
I am fond of this method, which requires no additional dependencies and needs only a few steps, and its isolation for each test is perfect.
Testify Mock
If you only look at its Mock part, it is somewhat similar to GoMock, and it also contains two tools.
- testify/mockwith various mock methods
- mockery used to generate mock code
Also, take mocking an interface as an example, you should take the following steps.
- Use mockery to generate mock code
type User struct {
action Abc
}
type Abc interface {
func DD(string) err
}
mockery -dir abc -name Abc
- Build mock objects in the test
mockAbc := &mocks.Abc{}
- Use mock
func TestWithTestifyMock(t *testing.T) {
mockAbc := &mocks.Abc{}testUser := &user.User{action:mockAbc}// Mock DD func with "abc" as parameters, and return nil from the mocked call.
mockAbc.On("DD", "abc").Return(nil).Once()// call the original logic include mock func
testUser.xxx()
}
- Assertion
Expect(testUser.xxx()).To(Succeed())
The process and experience using it are very similar to GoMock, and there is almost no difference. If you ask for a comparison, Testify has a slight advantage in result output and community activity. For more comparisons, please refer to this articleâµ.
In the general developing process, the choice depends more on the developer’s habit. Always use the one you’re most familiar with.
GoStub
GoStub framework supports many scenarios, including all the above.
Mock a global variable
Assuming that num is a global integer variable used in the function under test, suppose the value of num is greater than 10 in the current test case, for example, 20. And the Mock code is as follows:
stubs := Stub(&num, 20)
defer stubs.Reset()
stubs is the object returned by Stub, the function interface of the GoStub framework. And it restores the global variable’s value to the original one via the reset operation.
Mock a function
Assuming that the GetConfig method is to obtain information about the configuration file.
var GetConfig = func(fileName string) ([]byte, error) {
return ioutil.ReadFile(fileName)
}
Now mock the GetConfig function. 👇
// Test code
stubs := gostub.Stub(&configFile, "/tmp/test.config")data, err := GetConfig()
Mock is easier to complete when the method does not return a value, such as resource-cleaning functions.
The following is a Mock code of CleanUp.
stubs := StubFunc(&CleanUp)
defer stubs.Reset()
GoStub has a significant disadvantage, that is, its inability to mock the regular func definition. For example, the above GetConfig cannot be mocked if it is defined in the following way.
func GetConfig(fileName string) ([]byte, error) {
return ioutil.ReadFile(fileName)
}
Because GoStub uses reflection to handle the Mock, the Stub function’s first parameter has to be a pointer. Since the above function doesn’t have a pointer, and to make GoStub do its job, we need to change the code intrusively.
GoMonkey
GoMonkey is a monkey-patching framework of Golang.
It rewrites executable files through assembly statements at runtime and jumps the implementation of functions or methods to be mocked to stub implementation.
The principle is similar to that of hot patches. Through GoMonkey, we can solve the Mock problem of functions or methods.
There are many usage scenarios of the GoMonkey framework, which can almost meet all the requirements. Here is an example,
- Mock a function
func Exec(cmd string, args ...string) (string, error) {
cmdpath, err := exec.LookPath(cmd)
if err != nil {
fmt.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
return "", infra.ErrExecLookPathFailed
}
var output []byte
output, err = exec.Command(cmdpath, args...).CombinedOutput()
if err != nil {
fmt.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
return "", infra.ErrExecCombinedOutputFailed
}
fmt.Println("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
return string(output), nil
}
Monkey’s API is straightforward. Let’s look at the mock code directly.
func TestExec(t *testing.T) {
Convey("test has digit", t, func() {
Convey("for succ", func() {
outputExpect := "bb"
guard := Patch(
osencap.Exec,
func(_ string, _ ...string) (string, error) {
return outputExpect, nil
})
defer guard.Unpatch()
output, err := osencap.Exec("any", "any")
So(output, ShouldEqual, outputExpect)
So(err, ShouldBeNil)
})
})
}
Patch is an API that Monkey provides for function mocks:
- The first parameter is the function name of the objective function.
- The second parameter is the function name of the mock function. People often use an anonymous function or closure.
- The return value is a PatchGuard object pointer mainly used to delete the current patch after finishing the test.
Monkey works well and is easy to use, but it also has some weaknesses.
- Not thread-safe and not applicable to concurrent testing.
- Can’t support the inline function.
- Intrusive. The initials of the function name must be lowercase to fit its reflection mechanism (subsequent versions may fix this problem)
Summary
In real projects, you may need to introduce some of the above frameworks to complete all the mock jobs.
In my opinion, the best practices are,
- Go httptest, mock HTTP requests.
- GoMonkey, mock functions.
- GoMock/Testify, mock interfaces.
- GoMonkey, mock system internal functions.
Of course, you can also accomplish everything with a single framework since it all depends on the project’s actual situation. But having a better understanding of frameworks is of great importance when you make your choice.
There are more complex examples of using the frameworks mentioned above, which I will skip here. Those who are interested can refer to relevant official documents.
Hope this article can help you understand a little better about Mock behavior in Golang.
Thanks for reading!
Reference
Note: The post is authorized by original author to republish on our site. Original author is Stefanie Lai who is currently a Spotify engineer and lives in Stockholm, orginal post is published here.