In real projects, when you need to do unit testing. But often you find that there are a lot of dependencies. That’s where Gomock comes in handy!

Gomock is an official mock framework for the Go language

Installation

1
2
$ go get -u github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen
  • Step 1: We will install the gomock third-party library and the mock code generation tool, mockgen, which will save us a lot of work. Just to understand how to use it

  • Step 2: Enter mockgen to verify that the code generation tool is installed correctly. If it does not respond properly, please check if the bin directory contains the binary file

Usage

In the mockgen command, two generation modes are supported

  1. source: generate mock interface from source files (enabled via -source)
1
mockgen -source=foo.go [other options]
  1. reflect: generates the mock interface by using a reflection procedure. It is enabled by passing two non-flagged parameters: the import path and a comma-separated list of interfaces
1
mockgen database/sql/driver Conn,Driver

In essence, there is no difference between the mock code generated by the two approaches. So just choose the right one

Writing test cases

In this article, we will simulate a simple demo to write test cases and get familiar with the overall testing process.

Steps

  1. Figure out the overall logic
  2. Define the interface for the desired (simulated) dependencies
  3. Use the mockgen command to generate a mock file for the interface of the required mock
  4. Write the logic for the unit tests and use mock in the tests
  5. Perform unit test validation

Catalog

1
2
3
4
5
6
├── mock
├── person
│   └── male.go
└── user
    ├── user.go
    └── user_test.go

Writing

interface method

Open the person/male.go file and write the following.

1
2
3
4
5
package person

type Male interface {
	Get(id int64) error
}

Calling Methods

Open the user/user.go file and write the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package user

import "github.com/EDDYCJY/mockd/person"

type User struct {
	Person person.Male
}

func NewUser(p person.Male) *User {
	return &User{Person: p}
}

func (u *User) GetUserInfo(id int64) error {
	return u.Person.Get(id)
}

Generate a mock file

Go back to the root directory of mockd/ and execute the following command

1
$ mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock

After the execution, you can find the male_mock.go file in the mock/ directory, which is the mock file. What is the purpose of the commands in the command? As follows.

  • -source: set the interface file to be mocked
  • -destination: set where the mock file is output, or print to standard output if not set
  • -package: set the package name of the mock file, if not set it will be the mock_ prefix plus the file name (e.g. the package name in this article will be mock_person)

For more information on the command line, see the official documentation

The output mock file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Code generated by MockGen. DO NOT EDIT.
// Source: ./person/male.go

// Package mock is a generated GoMock package.
package mock

import (
	gomock "github.com/golang/mock/gomock"
	reflect "reflect"
)

// MockMale is a mock of Male interface
type MockMale struct {
	ctrl     *gomock.Controller
	recorder *MockMaleMockRecorder
}

// MockMaleMockRecorder is the mock recorder for MockMale
type MockMaleMockRecorder struct {
	mock *MockMale
}

// NewMockMale creates a new mock instance
func NewMockMale(ctrl *gomock.Controller) *MockMale {
	mock := &MockMale{ctrl: ctrl}
	mock.recorder = &MockMaleMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockMale) EXPECT() *MockMaleMockRecorder {
	return m.recorder
}

// Get mocks base method
func (m *MockMale) Get(id int64) error {
	ret := m.ctrl.Call(m, "Get", id)
	ret0, _ := ret[0].(error)
	return ret0
}

// Get indicates an expected call of Get
func (mr *MockMaleMockRecorder) Get(id interface{}) *gomock.Call {
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMale)(nil).Get), id)
}

Test cases

Open the user/user_test.go file and write the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package user

import (
	"testing"

	"github.com/EDDYCJY/mockd/mock"

	"github.com/golang/mock/gomock"
)

func TestUser_GetUserInfo(t *testing.T) {
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var id int64 = 1
	mockMale := mock.NewMockMale(ctl)
	gomock.InOrder(
		mockMale.EXPECT().Get(id).Return(nil),
	)

	user := NewUser(mockMale)
	err := user.GetUserInfo(id)
	if err != nil {
		t.Errorf("user.GetUserInfo err: %v", err)
	}
}
  1. gomock.NewController: Returns gomock.Controller, which represents the top-level control in the mock ecosystem. Defines the scope, lifecycle and expected values of a mock object. In addition it is safe across multiple goroutines
  2. mock.NewMockMale: Creates a new mock instance
  3. gomock.InOrder: declares that the given calls should be made in order (is a secondary wrapper around gomock.
  4. mockMale.EXPECT().Get(id).Return(nil): There are three steps here, EXPECT() returns an object that allows the caller to set the expectation and return value. get(id) sets the input and calls the method in the mock instance. return(nil) sets the output of the previously called method. Return(nil) is to set the reference of the previously called method. In short, it sets the entry and calls it, and finally sets the return value
  5. NewUser(mockMale): creates the User instance, notably, the mock object is injected here, so it’s actually in the subsequent user.GetUserInfo(id) call (input: id is 1). It calls the mock method that we have mocked beforehand
  6. ctl.Finish(): asserts the expected value of the mock use case, usually with deferral to prevent us from forgetting this operation

Testing

Go back to the root directory of mockd/ and execute the following command

1
2
$ go test ./user
ok  	github.com/EDDYCJY/mockd/user

When you see the result like this, you’re done! You can adjust the return value of Return() yourself to get different test results 😄

View the test

Test Coverage

1
2
$ go test -cover ./user
ok  	github.com/EDDYCJY/mockd/user	(cached)	coverage: 100.0% of statements

Coverage statistics can be turned on by setting the -cover flag to display coverage: 100.0%.

Visualization interface

  1. Generate a test coverage profile file
1
$ go test ./... -coverprofile=cover.out
  1. Generating visual interfaces with profile files
1
go tool cover -html=cover.out
  1. View visual interface to analyze coverage

-

More

1. Common mock methods

Calling Methods

  • Call.Do(): declares the action to be run when matching
  • Call.DoAndReturn(): declares the action to be run when matching the call and simulates returning the return value of the function
  • MaxTimes(): set the maximum number of calls to n
  • Call.MinTimes(): sets the minimum number of calls to n
  • AnyTimes(): allow 0 or more calls
  • Times(): set the number of calls to n

Parameter Matching

  • gomock.Any(): match any value
  • gomock.Eq(): match to the specified type value by reflection, no need to set it manually
  • gomock.Nil(): return nil

More suggested methods can be found in the official documentation

2. Generate multiple mock files

Officially, we can use go:generate to do batch processing in a more convenient way

1
go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

Modify the interface method

Open the person/male.go file and change it to the following.

1
2
3
4
5
6
7
package person

//go:generate mockgen -destination=../mock/male_mock.go -package=mock github.com/EDDYCJY/mockd/person Male

type Male interface {
	Get(id int64) error
}

We focus on the statement go:generate, which can be divided into the following parts.

  • Declare //go:generate (be careful not to leave spaces)
  • Use the mockgen command
  • Define -destination
  • Define -package
  • Define source, in this case the package path for person
  • Define -interfaces, in this case Male

Regenerate the mock file

Go back to the root directory of mockd/ and execute the following command

1
$ go generate ./...

Checking mock/ again shows that it has also been generated correctly, which is convenient when there are multiple files 🤩

Summary

In the unit testing loop, gomock gives us a great deal of convenience. The ability to mock a lot of dependencies

There are a lot of ways to use it and a lot of features. You can mark it and read the official documentation to remember it better


Reference https://eddycjy.com/posts/go/talk/2018-11-25-gomock/