08/16/17 by ย Sergey Grebenshchikov
This is a quick tutorial on how to test code using theย GoMockย mocking library and the standard library testing packageย testing.
GoMockย is a mock framework for Go. It enjoys a somewhat official status as part of theย github.com/golangย organization, integrates well with the built-inย testingย package, and provides a flexible expectation API.
The code snippets referenced in this post are available on GitHub:ย github.com/sgreben/testing-with-gomock.
- Installation
- Basic Usage
- Usingย GoMockย withย
go:generate - Using argument matchers
- Asserting call order
- Specifying mock actions
- Summary
First, we need to install theย gomockย packageย github.com/golang/mock/gomockย as well as theย mockgenย code generation toolย github.com/golang/mock/mockgen. Technically, weย couldย do without the code generation tool, but then weโd have to write our mocks by hand, which is tedious and error-prone.
Both packages can be installed usingย go get:
go get github.com/golang/mock/gomock go get github.com/golang/mock/mockgen
We can verify that theย mockgenย binary was installed successfully by running
$GOPATH/bin/mockgen
This should output usage information and a flag list. Now that weโre all set up, weโre ready to test some code!
Usage ofย GoMockย follows four basic steps:
- Useย
mockgenย to generate a mock for the interface you wish to mock. - In your test, create an instance ofย
gomock.Controllerย and pass it to your mock objectโs constructor to obtain a mock object. - Callย
EXPECT()ย on your mocks to set up their expectations and return values - Callย
Finish()ย on the mock controller to assert the mockโs expectations
Letโs look at a small example to demonstrate the above workflow. To keep things simple, weโll be looking at just two files โ an interfaceย Doerย in the fileย doer/doer.goย that we wish to mock and a structย Userย inย user/user.goย that uses theย Doerย interface.
The interface that we wish to mock is just a couple of lines โ it has a single methodย DoSomethingย that does something with anย intย and aย stringย and returns anย error:
doer/doer.go
package doer
type Doer interface { DoSomething(int, string) error }
Hereโs the code that we want to test while mocking out theย Doerย interface:
user/user.go
package user
import "github.com/sgreben/testing-with-gomock/doer"
type User struct { Doer doer.Doer }
func (u *User) Use() error { return u.Doer.DoSomething(123, "Hello GoMock") }
Our current project layout looks as follows:
'-- doer
'-- doer.go
'-- user
'-- user.goWeโll put the mock forย Doerย in a packageย mocksย in the root directory and the test forย Userย in the fileย user/user_test.go:
'-- doer
'-- doer.go
'-- mocks
'-- mock_doer.go
'-- user
'-- user.go
'-- user_test.goWe start by creating a directoryย mocksย that will contain our mock implementations and then runningย mockgenย on theย doerย package:
mkdir -p mocks
mockgen -destination=mocks/mock_doer.go -package=mocks github.com/sgreben/testing-with-gomock/doer Doer
Here, we have to create the directoryย mocksย ourselves because GoMock wonโt do it for us and will quit with an error instead. Hereโs what the arguments given toย mockgenย mean:
-destination=mocks/mock_doer.go: put the generated mocks in the fileยmocks/mock_doer.go.-package=mocks: put the generated mocks in the packageยmocksgithub.com/sgreben/testing-with-gomock/doer: generate mocks for this packageDoer: generate mocks for this interface. This argument is required โ we need to specify the interfaces to generate mocks for explicitly. Weย can, however specify multiple interfaces here as a comma-separated list (e.g.ยDoer1,Doer2).
Ifย $GOPATH/binย was not in ourย $PATH, weโd have to callย mockgenย viaย $GOPATH/bin/mockgen. In the following weโll assume that we haveย $GOPATH/binย in ourย $PATH.
As a result, our invocation ofย mockgenย places a fileย mocks/mock_doer.goย in our project. This is how such a generated mock implementation looks:
mocks/mock_doer.go
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/sgreben/testing-with-gomock/doer (interfaces: Doer)
package mocks
import (
gomock "github.com/golang/mock/gomock"
)
// MockDoer is a mock of Doer interface
type MockDoer struct {
ctrl *gomock.Controller
recorder *MockDoerMockRecorder
}
// MockDoerMockRecorder is the mock recorder for MockDoer
type MockDoerMockRecorder struct {
mock *MockDoer
}
// NewMockDoer creates a new mock instance
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
mock := &MockDoer{ctrl: ctrl}
mock.recorder = &MockDoerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (_m *MockDoer) EXPECT() *MockDoerMockRecorder {
return _m.recorder
}
// DoSomething mocks base method
func (_m *MockDoer) DoSomething(_param0 int, _param1 string) error {
ret := _m.ctrl.Call(_m, "DoSomething", _param0, _param1)
ret0, _ := ret\[0\].(error)
return ret0
}
// DoSomething indicates an expected call of DoSomething
func (_mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "DoSomething", arg0, arg1)
}Note that the generatedย EXPECT()ย method is defined on the same object as the mock methods (in this case,ย DoSomething) โ avoiding name clashes here is likely a reason for the non-standard all-uppercase name.
Next, we define aย mock controllerย inside our test. A mock controller is responsible for tracking and asserting the expectations of its associated mock objects.
We can obtain a mock controller by passing a valueย tย of typeย *testing.Tย to its constructor, and then use it to construct a mock of theย Doerย interface. We alsoย deferย itsย Finishย method โ more on this later.
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockDoer := mocks.NewMockDoer(mockCtrl)Suppose we want to assert thatย mockerDoerโsย Doย method will be calledย once, withย 123ย andย "Hello GoMock"ย as arguments, and will returnย nil.
To do this, we can now callย EXPECT()ย on theย mockDoerย to set up its expectations in our test. The call toย EXPECT()returns an object (called a mockย recorder) providing methods of the same names as the real object.
Calling one of the methods on the mock recorder specifies an expected call with the given arguments. You can then chain other properties onto the call, such as:
- the return value (viaย
.Return(...)) - the number of times this call is expected to occur (viaย
.Times(number), or viaย.MaxTimes(number)ย andย.MinTimes(number))
In our case, the call looks like this:
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)
Thatโs it โ weโve now specified our first mock call! Hereโs the complete example:
user/user_test.go
package user_test
import (
"github.com/sgreben/testing-with-gomock/mocks"
"github.com/sgreben/testing-with-gomock/user"
)
func TestUse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockDoer := mocks.NewMockDoer(mockCtrl)
testUser := &user.User{Doer:mockDoer}
// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return nil from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)
testUser.Use()
}Probably, itโs not obvious where in the code the mockโs expectations are asserted. This happens in the deferredย Finish(). Itโs idiomatic toย deferย this call toย Finishย at the point of declaration of the mock controller โ this way we donโt forget to assert the mock expectations later.
Finally, weโre ready to run our tests:
$ go test -v github.com/sgreben/testing-with-gomock/user
=== RUN TestUse
--- PASS: TestUse (0.00s)
PASS
ok github.com/sgreben/testing-with-gomock/user 0.007sIf you need to construct more than one mock, you can reuse the mock controller โ itsย Finishย method will then assert the expectations of all mocks associated with the controller.
We might also want to assert that the value returned by theย Useย method is indeed the one returned to it byย DoSomething. We can write another test, creating a dummy error and then specifying it as a return value forย mockDoer.DoSomething:
user/user_test.go
func TestUseReturnsErrorFromDo(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dummyError := errors.New("dummy error")
mockDoer := mocks.NewMockDoer(mockCtrl)
testUser := &user.User{Doer:mockDoer}
// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return dummyError from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(dummyError).Times(1)
err := testUser.Use()
if err != dummyError {
t.Fail()
}
}Runningย mockgenย for each package and interface individually is cumbersome when there is a large number of interfaces/packages to mock. To alleviate this problem, theย mockgenย command may be placed in a specialย go:generateย comment.
In our example, we can add aย go:generateย comment just below theย packageย statement of ourย doer.go:
doer/doer.go
package doer
//go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks github.com/sgreben/testing-with-gomock/doer Doer
type Doer interface {
DoSomething(int, string) error
}Note that at the point whereย mockgenย is called, the current working directory isย doerย โ hence we need to specifyย ../mocks/ย as the directory to write our mocks to, not justย mocks/.
We can now comfortably generate all mocks specified by such a comment by running
go generate ./...from the projectโs root directory. Note that there is no space betweenย //ย andย go:generateย in the comment. This is required forย go generateย to pick up the comment as an instruction to process.
A reasonable policy on where to put theย go:generateย comment and which interfaces to include is the following:
- Oneย
go:generateย comment per file containing interfaces to be mocked - Include all interfaces to generate mocks for in the call toย
mockgen - Put the mocks in a packageย
mocksย and write the mocks for a fileยX.goย intoยmocks/mock_X.go.
This way, theย mockgenย call is close to the actual interfaces, while avoiding the overhead of separate calls and destination files for each interface.
Sometimes, you donโt care about the specific arguments a mock is called with. Withย GoMock, a parameter can be expected to have a fixed value (by specifying the value in the expected call) or it can be expected to match a predicate, called aย Matcher. Matchers are used to represent ranges of expected arguments to a mocked method. The following matchers are pre-defined inย GoMock:
gomock.Any(): matches any value (of any type)gomock.Eq(x): uses reflection to match values that areยDeepEqualย toยxgomock.Nil(): matchesยnilgomock.Not(m): (whereยmย is a Matcher) matches values not matched by the matcherยmgomock.Not(x): (whereยxย isย notย a Matcher) matches values notยDeepEqualย toยx
For example, if we donโt care about the value of the first argument toย Do, we could write:
mockDoer.EXPECT().DoSomething(gomock.Any(), "Hello GoMock")
GoMockย automatically converts arguments that areย notย of typeย Matcherย toย Eqย matchers, so the above call is equivalent to:
mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))
You can define your own matchers by implementing theย gomock.Matcherย interface:
gomock/matchers.goย (excerpt)
type Matcher interface { Matches(x interface{}) bool String() string }
Theย Matchesย method is where the actual matching happens, whileย Stringย is used to generate human-readable output for failing tests. For example, a matcher checking an argumentโs type could be implemented as follows:
match/oftype.go
package match
import (
"reflect"
"github.com/golang/mock/gomock"
)
type ofType struct{ t string }
func OfType(t string) gomock.Matcher {
return &ofType{t}
}
func (o *ofType) Matches(x interface{}) bool {
return reflect.TypeOf(x).String() == o.t
}
func (o *ofType) String() string {
return "is of type " + o.t
}We can then use our custom matcher like this:
// Expect Do to be called once with 123 and any string as parameters, and return nil from the mocked call.
mockDoer.EXPECT().
DoSomething(123, match.OfType("string")).
Return(nil).
Times(1)Weโve split the above call across multiple lines for readability. For more complex mock calls this is a handy way of making the mock specification more readable. Note that in Go we have to put the dot at theย endย of each line in a sequence of chained calls. Otherwise, the parser will consider the line ended and weโll get a syntax error.
The order of calls to an object is often important.ย GoMockย provides a way to assert that one call must happen after another call, theย .Afterย method. For example,
callFirst := mockDoer.EXPECT().DoSomething(1, "first this")
callA := mockDoer.EXPECT().DoSomething(2, "then this").After(callFirst)
callB := mockDoer.EXPECT().DoSomething(2, "or this").After(callFirst)specifies thatย callFirstย must occur before eitherย callAย orย callB.
GoMockย also provides a convenience functionย gomock.InOrderย to specify that the calls must be performed in the exact order given. This is less flexible than usingย .Afterย directly, but can make your tests more readable for longer sequences of calls:
gomock.InOrder(
mockDoer.EXPECT().DoSomething(1, "first this"),
mockDoer.EXPECT().DoSomething(2, "then this"),
mockDoer.EXPECT().DoSomething(3, "then this"),
mockDoer.EXPECT().DoSomething(4, "finally this"),
)Underย the hood,ย InOrderย usesย .Afterย to chain the calls in sequence.
Mock objects differ from real implementations in that they donโt implement any of their behavior โ all they do is provide canned responses at the appropriate moment and record their calls. However, sometimes you need your mocks to do more than that. Here,ย GoMockโsย Doย actions come in handy. Any call may be decorated with an action by callingย .Doย on the call with a function to be executed whenever the call is matched:
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
fmt.Println("Called with x =",x,"and y =", y)
})Complex assertions about the call arguments can be written insideย Doย actions. For example, if the first (int) argument ofย DoSomethingย should be less than or equal to the length of the second (string) argument, we can write:
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
if x > len(y) {
t.Fail()
}
})The same functionality couldย notย be implemented using custom matchers, since we areย relatingย the concrete values, whereas matchers only have access to one argument at a time.
In this post, weโve seen how to generate mocks usingย mockgenย and how to batch mock generation usingย go:generateย comments and theย go generateย tool. Weโve covered the expectation API, including argument matchers, call frequency, call order andย Do-actions.
If you have any questions or if you feel that thereโs something missing or unclear, please donโt hesitate to let me know in the comments!
Very well documented . Thank you ๐