Unit Testing in Go using GoMocks
GoMocks
Mocking may sound a little bit tedious and time-consuming at first, but can be implemented easily with the help of a pre-existing mocking framework.
By mocking the API, you can validate all the functionality of your application without the performance of the real API playing a role. In this blog, we’ll explore the GoMock framework, how to implement GoMock in an application alongside the benefits and limitations of mocks.
GoMock is a mock framework for Go. It is useful for mocking an interface. GoMock isn’t the only Golang testing framework, but it’s the preferred tool for many because of its powerful type-safe assertions.
Using go-mock framework in golang, we can generate mock objects of entire interfaces with just one line of command. GoMock works by generating a Mock Object from interfaces, meaning GoMock requires interfaces to function. Generating a Mock Object is the creation of a new file that contains an implementation of your interface and all the logic needed to create stubs and define mock behaviour.
The next sections will explain how to install and use GoMock to create and manage mock objects for unit testing in Go.
Features
GoMock provides several features that make it a powerful tool for writing advanced unit tests in Go.
automatically generates mock objects based on the interfaces defined, which represent the behaviour you want to mock.
GoMock also provides tools for recording the calls made to mock objects, making it easier to verify that your code is behaving as expected.
Prerequisites
Before you dive into the next sections, please make sure you meet the following prerequisites:
Basic understanding of the Go language (not strictly necessary but if you know Go already then this blog would be more beneficial, If you don't know then start learning at GoDev)
A system with Go installed
Installation
First, we need to install the gomock
package as well as the mockgen
code generation tool. Technically, we can mock objects manually without the code generation tool, but then we’d have to write our mocks by hand, which is tedious and error-prone. Hence we use the mock code generation tool: mockgen
, which will save us a lot of work.
Install the packages using the below commands:
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
we can verify mockgen installation by checking whether the mockgen binary file exists in the bin path ($GOPATH/bin/mockgen)
or by running mockgen
command in the terminal
Example
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 valuesCall
Finish()
on the mock controller to assert the mock’s expectations
Let’s look at a small example to demonstrate the above flow. To make it simple and understandable, we’ll be looking at just two files, an interface rectangle.go
in the package shapes
that we wish to mock and a struct paint
in paint
package that uses the rectangle
interface.
The interface we wish to mock contains a single method- area()
with params width & height
shapes/rectangle.go
package shapes
type Rectangle interface {
Area(width int, height int) error
}
paint/paint.go
package paint
import "example/shapes"
type Paint struct {
Shapes shapes.Rectangle
}
func NewPaint(s shapes.Rectangle) *Paint {
return &Paint{Shapes: s}
}
func (u *Paint) GetArea(w int, h int) error {
return u.Shapes.Area(w, h)
}
We will now start by creating a directory mocks
that will contain our mock implementations and then running mockgen
on the shapes
package:
go to the root directory and run the following command to generate mock object for interface rectangle.
mockgen -source=./shapes/rectangle.go -destination=./mock/rectangle_mock.go -package=mocks
It will generate file rectangle_mock.go
inside folder mocks
based on shapes/rectangle.go
using the package named mocks
.
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
represent:
-source
: set the interface file to be mocked-destination
: set the path of output file-package
: package of the generated code; defaults to the package of the input with a ‘mock_’ prefix. Set the package name of the mock file, if not set it will be themock_
prefix plus the package name (e.g. mock_shapes)
The generated output mock file is:
// Code generated by MockGen. DO NOT EDIT.
// Source: ./shapes/rectangle.go
// Package mock is a generated GoMock package.
package mock
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockRectangle is a mock of Rectangle interface.
type MockRectangle struct {
ctrl *gomock.Controller
recorder *MockRectangleMockRecorder
}
// MockRectangleMockRecorder is the mock recorder for MockRectangle.
type MockRectangleMockRecorder struct {
mock *MockRectangle
}
// NewMockRectangle creates a new mock instance.
func NewMockRectangle(ctrl *gomock.Controller) *MockRectangle {
mock := &MockRectangle{ctrl: ctrl}
mock.recorder = &MockRectangleMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRectangle) EXPECT() *MockRectangleMockRecorder {
return m.recorder
}
// Area mocks base method.
func (m *MockRectangle) Area(width, height int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Area", width, height)
ret0, _ := ret[0].(error)
return ret0
}
// Area indicates an expected call of Area.
func (mr *MockRectangleMockRecorder) Area(width, height interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Area", reflect.TypeOf((*MockRectangle)(nil).Area), width, height)
}
After generating the mock, you’re ready to start using mocks inside your tests. Create the test file for paint
in the path paint/paint_test.go
and write tests.
Now the folder structure will look like this:
-- mocks
-- rectangle_mock.go
-- paint
-- paint.go
-- paint_test.go
-- shapes
-- rectangle.go
paint/paint_test.go
package paint
import (
"example/mock"
"testing"
"github.com/golang/mock/gomock"
)
func TestPaint_GetArea(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockShape := mock.NewMockRectangle(mockCtrl)
// Expect Area to be called once with 10, 10 as parameters.
mockShape.EXPECT().Area(10, 10).Times(1)
testPaint := NewPaint(mockShape)
err := testPaint.GetArea(10, 10)
if err != nil {
t.Errorf("paint.GetArea err: %v", err)
}
}
gomock.NewController
: Returnsgomock.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 goroutinesmock.NewMockRectangle
: Creates a new rectangle mock instancemockShape.EXPECT().Area(10, 10).Times(1)
: There are three steps here, EXPECT() returns an object that allows the caller to set the expectation. Area(10,10) sets the input and calls the method in the mock instance. Times(1) sees to call the Area method once.NewPaint(mockShape)
: creates the Paint instance, notably, the mock object is injected here.ctrl.Finish()
: asserts the expected value of the mock use case, usually withdeferral
to prevent us from forgetting this operation. New ingo1.14+
, if you are passing a*testing.T
into this function you no longer need to callctrl.Finish()
in your test methods.
Test result:
Run the command to see the test result:
go test ./paint
you could see something like this, which means the test passed
To see the test coverage, you can run the command:
go test -cover ./paint
and check output as:ok example/paint 0.425s coverage: 100.0% of statements
To visually see the test coverage, you can try the following steps:
Generate a test coverage profile file
go test ./... -coverprofile=cover.out
Generate visual interfaces with profile files
go tool cover -html=cover.out
View visual interface to analyze coverage
Limitations
As we have seen how to implement GoMock in an application and its benefits, this framework also has a few limitations along with benefits.
Only Works with Interfaces
Requires Significant Setup
Please leave your feedback in comments section.
Thank you for reading!!
Subscribe to my newsletter
Read articles from SHIVANI GANIMUKKULA directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by