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:

  1. Use mockgen to generate a mock for the interface you wish to mock.

  2. In your test, create an instance of gomock.Controller and pass it to your mock object’s constructor to obtain a mock object.

  3. Call EXPECT() on your mocks to set up their expectations and return values

  4. Call 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 the mock_ 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)
    }
}
  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.NewMockRectangle: Creates a new rectangle mock instance

  3. mockShape.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.

  4. NewPaint(mockShape): creates the Paint instance, notably, the mock object is injected here.

  5. ctrl.Finish(): asserts the expected value of the mock use case, usually with deferral to prevent us from forgetting this operation. New in go1.14+, if you are passing a *testing.T into this function you no longer need to call ctrl.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!!

1
Subscribe to my newsletter

Read articles from SHIVANI GANIMUKKULA directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SHIVANI GANIMUKKULA
SHIVANI GANIMUKKULA