Skip to main content

Mock Me, Please!!!

Photo by Nicole Wreyford on Unsplash

Back in my early day as a software engineer, I used to have two development databases, one for development (yeah, obviously) and another one for unit testing. I never knew what is mocking, and how to use it, and yet I never mock any piece of my code for unit testing purposes simply because I don’t understand the idea of a unit test.

In this jot, I want to talk about how to use GoMock, a tool by Go, to mock your external dependencies.

What is 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. - golang/mock

To install GoMock, you can use go install’s command:

go install github.com/golang/mock/[email protected]

Once you installed GoMock, you can use the GoMock’s CLI using command mockgen on your terminal.

mockgen command

Let’s Start Mocking!

Let’s say you have a service layer. Your service layer has a dependency on the repository layer that include DB connection.

project
│   README.md
└───service
│   │   library.go

Since the purpose of a unit test is to test your unit of code (function, method, etc…), you don’t need the actual implementation of the repository layer, you only want to make sure that the behavior of the code that you wrote is working as you expect, here’s come the mocking part.

package service

import (
	"context"

	"github.com/mhdiiilham/go-101/model"
)

type BookRepository interface {
	GetBooks(ctx context.Context) (book []model.Book, err error)
}

type LibraryService struct {
	repository BookRepository
}

func NewLibraryService(repository BookRepository) *LibraryService {
	return &LibraryService{repository: repository}
}

func (s *LibraryService) GetBooks(ctx context.Context) (books []model.Book, err error) {
	books, err = s.repository.GetBooks(ctx)
	if err != nil {
		return nil, err
	}

	return books, nil
}

To test GetBooks method, first, we need to generate the mock instance of BookRepository.

There’s two way on how to generate the mock.

How To Create The Mock

1. Reflect mode

Reflect mode generates mock interfaces by building a program that uses reflection to understand interfaces. It is enabled by passing two non-flag arguments: an import path, and a comma-separated list of symbols.

You can use “.” to refer to the current path’s package.

To create the mock using go:generate command, you need to add go:generate on top of your interface.

//go:generate mockgen -destination=mock/mock_repository.go -package=mock . BookRepository
type BookRepository interface {
	GetBooks(ctx context.Context) (book []model.Book, err error)
}

then run command

go generate ./...
Error

You might (or might not) get an error like this

$ go generate ./...
prog.go:12:2: no required module provides package github.com/golang/mock/mockgen/model; to add it:
        go get github.com/golang/mock/mockgen/model
prog.go:12:2: no required module provides package github.com/golang/mock/mockgen/model; to add it:
        go get github.com/golang/mock/mockgen/model
prog.go:12:2: no required module provides package github.com/golang/mock/mockgen/model: go.mod file not found in current directory or any parent directory; see 'go help modules'
prog.go:14:2: no required module provides package github.com/mhdiiilham/go-101/service: go.mod file not found in current directory or any parent directory; see 'go help modules'
2023/02/25 13:46:24 Loading input failed: exit status 1
service/library.go:3: running "mockgen": exit status 1

To solve this error, just run the go get command:

go get github.com/golang/mock/mockgen/model

2. Source mode

Source mode generates mock interfaces from a source file. It is enabled by using the -source flag.

example

mockgen -source=service/library.go -destination=service/mock/mock_book_repository.go -package=mock

These two will create new direcory under your service/ with generate file that contained your mock instance of BookRepository.

project
│   README.md
└───service
│   │   library.go
│   └───mock
│       │   mock_repository.go

The Func Part! 😺

Let’s start testing our function…

package service_test

import (
	"context"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/mhdiiilham/go-101/model"
	"github.com/mhdiiilham/go-101/service"
	"github.com/mhdiiilham/go-101/service/mock"
	"github.com/stretchr/testify/assert"
)

func TestLibraryServiceGetBooks(t *testing.T) {
	ctx := context.Background()
	assertion := assert.New(t)

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	expectedBooks := []model.Book{
		{Title: "Inferno", Author: "Dan Brown"},
		{Title: "The Subtle Art Of Not Giving A F#", Author: "Mark Mason"},
	}

	mockRepository := mock.NewMockBookRepository(ctrl)

	service := service.NewLibraryService(mockRepository)

	books, err := service.GetBooks(ctx)
	assertion.NoError(err)
	assertion.Equal(expectedBooks, books)
}

Explantion:

  • ctrl: A Controller represents the top-level control of a mock ecosystem.
  • mockRepository: The mock instance that you going to use as the replacement of the actual repository.

If you run the test, It’ll fail because we haven’t use the mock instance, yet.

We need to customize the behavior of the mock repository, what argument is passed into the function, what are the returns, and how many times it is called. To do that, check this code snippet…

mockRepository := mock.NewMockBookRepository(ctrl)

mockRepository.
		EXPECT().
		GetBooks(ctx).
		Return(expectedBooks, nil).
		Times(1)

On the mock instance, we EXPECT that we call the GetBooks method with argument ctx and the method should Return expectedBooks and nil (we don’t expect any error).

Other example if we want to test how our function handle the error:

mockRepository.
		EXPECT().
		GetBooks(ctx).
		Return(nil, sql.ErrNoRows).
		Times(1)

Conclusion

In conclusion, GoMock is a powerful mocking framework for the Go programming language that enables developers to mock external dependencies, and unit test their code without relying on the actual implementation of the dependencies. With GoMock, developers can create mock instances of their dependencies and test the behavior of their code as expected.

comments powered by Disqus