Source From Here
PrefaceThis 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
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:
We can verify that the mockgen binary was installed successfully by running:
This should output usage information and a flag list. Now that we’re all set up, we’re ready to test some code!
Basic Usage
Usage of GoMock follows four basic steps:
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
- }
- package user
- import "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:
We start by creating a directory mocks that will contain our mock implementations and then running mockgen on the doer package:
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:
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: testing-with-gomock/doer (interfaces: Doer)
- // Package mocks is a generated GoMock package.
- package mocks
- import (
- reflect "reflect"
- 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(arg0 int, arg1 string) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DoSomething", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
- }
- // DoSomething indicates an expected call of DoSomething.
- func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
- }
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)
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:
In our case, the call looks like this:
- // 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)
- user/user_test.go
- package user_test
- import (
- "testing"
- "github.com/golang/mock/gomock"
- "testing-with-gomock/mocks"
- "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()
- }
Finally, we’re ready to run our tests:
If 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 testing-with-gomock/doer Doer
- type Doer interface {
- DoSomething(int, string) error
- }
We can now comfortably generate all mocks specified by such a comment by running:
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:
This way, the mockgen call is close to the actual interfaces, while avoiding the overhead of separate calls and destination files for each interface.
Using argument matchers
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:
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")
- mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))
- 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
- }
// 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)
Asserting call order
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)
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"),
- )
Specifying mock actions
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)
- })
- mockDoer.EXPECT().
- DoSomething(gomock.Any(), gomock.Any()).
- Return(nil).
- Do(func(x int, y string) {
- if x > len(y) {
- t.Fail()
- }
- })
Summary
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.
Supplement
* Youtube - How to mock in your Go tests - Golang Tutorial
* Youtube - Unit Tests and Test Doubles like Mocks, Stubs & Fakes
沒有留言:
張貼留言