All Articles

Mocking in golang? How to make unit-test more simple?

Unit-testing in go? How to make it more simple?

What is unit-test and why we need it?

Unit-test is used to test for the atomic level of the system under test (SUT). A unit may be an individual function, method, procedure, module, or object.

The most important benefit of unit-test is validate each unit performs as expected. It also help find out bugs, or incorrect logic inside of unit. And well-coded unit-test can be a good document of code.

To do unit-test we must isolate the unit of code, to archive this, we have Mock, Stub, and Fake.

Mock object

Mocking is a technique to create a mock object to simulate the behaviors of real objects, but with our controls. Mock object are programmed to response expected results from the expected input, and it will validate the input and output of function, it also asserts how many time it has been called. In unit-test, mocking is often using for the de-couple modules of the system, so that we just needed to test the logic and workflow business of each one.

Stub

Stub are like Mock, but it doesn’t make any assertions, just return the response as it programmed.

Fake

Fake objects is a ‘lightweight’ version of a real module in the system. It has own implementation, and act the same way as the real one but cannot use in production. For example, to test a third party’s service API, we use Fake for testing it, not the real one.

Make unit-test much more simpler with gomock

GoMock is a mocking framework for the Go programming language. It integrates well with Go’s built-in testing package, but can be used in other contexts too.

To install latest release version of gomock

GO111MODULE=on go get github.com/golang/mock/mockgen@v1.4.4

After installing success we can use mockgen to create mock for our code.

mockgen will work in two mode: Source and Reflect

Let say we have example here:

package structure

type Stacker interface {
	Append(interface{}) []interface{}
}

type Queuer interface {
	Enque(interface{}) []interface{}
}

Source mode

When we use the flag -source that mean have using the source mode. And the -source will specify the file containing interfaces to be mocked. That may be lead to some proplems when we don’t want to mock some interface. To prevent that, we can use reflect mode instead. And we can add more flags, then we can customize the output as the way we want.

For example:

mockgen -source=structure.go

After run this command, the mocked will be printed to standard output (your console) and as you can see here, two mock objects MockStacker, MockQueuer are created.

// Code generated by MockGen. DO NOT EDIT.
// Source: structure.go

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

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

// MockStacker is a mock of Stacker interface
type MockStacker struct {
	ctrl     *gomock.Controller
	recorder *MockStackerMockRecorder
}

// MockStackerMockRecorder is the mock recorder for MockStacker
type MockStackerMockRecorder struct {
	mock *MockStacker
}

// NewMockStacker creates a new mock instance
func NewMockStacker(ctrl *gomock.Controller) *MockStacker {
	mock := &MockStacker{ctrl: ctrl}
	mock.recorder = &MockStackerMockRecorder{mock}
	return mock
}

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

// Append mocks base method
func (m *MockStacker) Append(arg0 interface{}) []interface{} {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Append", arg0)
	ret0, _ := ret[0].([]interface{})
	return ret0
}

// Append indicates an expected call of Append
func (mr *MockStackerMockRecorder) Append(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Append", reflect.TypeOf((*MockStacker)(nil).Append), arg0)
}

// MockQueuer is a mock of Queuer interface
type MockQueuer struct {
	ctrl     *gomock.Controller
	recorder *MockQueuerMockRecorder
}

// MockQueuerMockRecorder is the mock recorder for MockQueuer
type MockQueuerMockRecorder struct {
	mock *MockQueuer
}

// NewMockQueuer creates a new mock instance
func NewMockQueuer(ctrl *gomock.Controller) *MockQueuer {
	mock := &MockQueuer{ctrl: ctrl}
	mock.recorder = &MockQueuerMockRecorder{mock}
	return mock
}

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

// Enque mocks base method
func (m *MockQueuer) Enque(arg0 interface{}) []interface{} {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Enque", arg0)
	ret0, _ := ret[0].([]interface{})
	return ret0
}

// Enque indicates an expected call of Enque
func (mr *MockQueuerMockRecorder) Enque(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enque", reflect.TypeOf((*MockQueuer)(nil).Enque), arg0)
}

Reflect mode

Reflect mode will created mock for only listed name of interface in package. This mode required only two non-flag arguments: an import path, and a comma-separated list of interface name.

For example:

mockgen . Queuer, Stacker # is "." is referenced to the current path's package, this is good for using with go generate
# Or mockgen github.com/kurojs/gomock Queuer,Stacker

These will generate the same response as the first example.

Flags for mockgen

The mockgen support for these flag, which are usefully to generate Mock for given class

-source: A file containing interfaces to be mocked. Cannot use with Reflect mode.

-destination: A file to which to write the resulting source code. If you don't set this, the code is printed to standard output.
High recommented given full package path here. Example: -destination=github.com/kurojs/gomock/mock_something.go

-package: The package to use for the resulting mock class source code. If you don't set this, the package name is mock_ concatenated with the package of the input file.

-imports: A list of explicit imports that should be used in the resulting source code, specified as a comma-separated list of elements of the form foo=bar/baz, where bar/baz is the package being imported and foo is the identifier to use for the package in the generated source code.

-aux_files: A list of additional files that should be consulted to resolve e.g. embedded interfaces defined in a different file. This is specified as a comma-separated list of elements of the form foo=bar/baz.go, where bar/baz.go is the source file and foo is the package name of that file used by the -source file.

-build_flags: (reflect mode only) Flags passed verbatim to go build.

-mock_names: A list of custom names for generated mocks. This is specified as a comma-separated list of elements of the form Repository=MockSensorRepository,Endpoint=MockSensorEndpoint, where Repository is the interface name and MockSensorRepository is the desired mock name (mock factory method and mock recorder will be named after the mock). If one of the interfaces has no custom name specified, then default naming convention will be used.

-self_package: The full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. This can happen if the mock's package is set to one of its inputs (usually the main one) and the output is stdio so mockgen cannot detect the final output package. Setting this flag will then tell mockgen which import to exclude.

-copyright_file: Copyright file used to add copyright header to the resulting source code.

Examples

Using as Mock
mock := mock.NewMockStacker(ctrl)
// Assert mock are called 3 times
for i := 1; i <= 3; i++ {
	mock.EXPECT().Append(gomock.Eq(i))
}
// And return 3
mock.EXPECT().Pop().Return(3)
Using as Stub
mock := mock.NewMockStacker(ctrl)
// No assertion, just return the expected data
mock.EXPECT().Append(gomock.Any()).AnyTimes()
mock.EXPECT().Pop().DoAndReturn(func() interface{} {
	return 3
})

You can find the example here: https://github.com/kurojs/gomock_demo

References

Published Nov 24, 2020

Software Engineer with enthusiasm and heuristics