Mocking with Mockery in Golang
Adding unit tests is paramount when it comes to ensuring the reliability and maintainability of the code written. It not only serves as a safety net to catch bugs early, but also safeguards future integrations. Pertaining to writing unit tests, mocking becomes an even more indispensable tool. Mocking facilitates isolation of specific components and emulating their behavior, creating controlled environments for testing without relying on external dependencies. We will be exploring how to create mocks using Mockery, a popular mocking library for Go.
What is Mockery?
Mockery is a tool that generates mock implementations of interfaces. Please do keep in mind that the interfaces need to be present, to be able to be mocked. Mockery heavily relies on interfaces to substitute real implementations with mocks. It enhances modularity, enables test isolation and facilitates dynamic behavior replacement during testing. We will understand more on this with the example mentioned later.
Github link can be found in Mockery.
Salient Features of Mockery
-
Automatic mock generation: Mockery automates the process of creating mock implementations for interfaces. You simply provide the interface you want to mock and Mockery generates the corresponding mock implementation. This can save a lot of time and effort, especially when dealing with interfaces that have numerous methods.
-
Simple command-line interface: Being a command-line tool, its usage is pretty straightforward. Just run a command with desired options, you will have your mocks generated. Not only that, we can customize various aspects too. We will understand further when we deep dive on the options/flags.
-
Support for testify/mock assertions: The generated mocks can easily be integrated with the assertions provided by the ‘testify/mock’ package, allowing us to seamlessly use ‘On’, ‘Return’, ‘AssertExpectations’ etc.
-
Mocking unexported interfaces: Mockery supports generation of mocks for unexported interfaces. This is quite useful when you have internal interfaces that you want to mock for testing purposes, but you don’t want to expose them to the external API of your package.
Getting started with Mockery
Before starting, we need to make sure to have Mockery installed. We can do it using below command :
go get github.com/vektra/mockery/v2/../
Once installed, we can use Mockery to generate mocks for the interfaces.
Mockery Commands
1. Generate Mocks for All interfaces
To generate mocks for all interfaces in the current directory and its subdirectories
mockery --all
2. Generate Mocks for a Specific interface
To generate mocks for a specific interface, we can use name flag
mockery --name InterfaceName
3. Specify output directory
By default, Mockery generates mocks in the ./mocks directory. We can use output flag to specify different output directory.
mockery --all --output path/to/output
4. Include subdirectories
Include subdirectories when generating mocks, we can use recursive flag.
mockery --all --recursive
5. Specify package name
Use the keeptree flag to preserve the directory structure when generating mocks. This can be useful to maintain the same package structure in mocks directory.
mockery --all --keeptree
6. Generate Mocks for a specific package
Create mocks for interfaces defined in a specific package. Handy in combination with output flag.
mockery --all --dir path/to/package
7. Define Output Package Name
To set custom package name for the generated mocks, we can use outputpkg flag.
mockery --all --outputpkg customMocks
Example: Using Mockery to test an interface
Let’s consider we have an interface DataProvider which represents an external service to fetch data. In our case, we will just return a random number between 0 and the number passed.
// data_provider.go
package service
// DataProvider is an interface for fetching and retrieving data.
type DataProvider interface {
GetRandomNumber(id int) (int, error)
}
Let’s create the implementation of the interface DataProvider to return a random number.
// data_provider_impl.go
package service
import (
"errors"
"math/rand"
)
// DataProviderImpl is the concrete implementation of the DataProvider interface.
type DataProviderImpl struct{}
// GetRandomNumber simulates fetching a random number between 0 and id.
func (d *DataProviderImpl) GetRandomNumber(id int) (int, error) {
if id < 0 {
return 0, errors.New("Invalid id")
}
// Simulate fetching a random number between 0 and id
return rand.Intn(id + 1), nil
}
Now, we need to consume the DataProvider to get the random data. Let’s create that. Additionally, we will check if the random number we fetched, is even or odd.
// data_consumer.go
package service
// ConsumeData is a function that uses a DataProvider to fetch and process data.
func ConsumeData(provider DataProvider, id int) (string, error) {
// Use GetRandomNumber to get a random number between 0 and id
randomNumber, err := provider.GetRandomNumber(id)
if err != nil {
return "", err
}
// Check whether the value is even or odd
result := checkEvenOrOdd(randomNumber)
// Return the result
return result, nil
}
// checkEvenOrOdd checks whether the given value is even or odd.
func checkEvenOrOdd(value int) string {
if value % 2 == 0 {
return "Even"
}
return "Odd"
}
Now, let’s create the mocks for DataProvider interface using Mockery. We will use below command to do so.
mockery --output ./mocks --dir ./service --all
On running the command, you will see below execution. What this will do, is create a DataProvider.go inside mocks package since we have specified –output ./mocks in the mockery command.
22 Jan 24 08:33 IST INF Starting mockery dry-run=false version=v2.36.1
22 Jan 24 08:33 IST INF Using config: dry-run=false version=v2.36.1
22 Jan 24 08:33 IST INF Walking dry-run=false version=v2.36.1
22 Jan 24 08:33 IST INF Generating mock dry-run=false interface=DataProvider qualified-name=RandomizedEvenOdd/service version=v2.36.1
22 Jan 24 08:33 IST INF writing mock to file dry-run=false interface=DataProvider qualified-name=RandomizedEvenOdd/service version=v2.36.1
Auto generated mocked DataProvider.go will be like below. Please make sure to re-generate mocks whenever there is an update in the interfaces. For example, we add a new function in the interface.
// Code generated by mockery v2.36.1. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// DataProvider is an autogenerated mock type for the DataProvider type
type DataProvider struct {
mock.Mock
}
// GetRandomNumber provides a mock function with given fields: id
func (_m *DataProvider) GetRandomNumber(id int) (int, error) {
ret := _m.Called(id)
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func(int) (int, error)); ok {
return rf(id)
}
if rf, ok := ret.Get(0).(func(int) int); ok {
r0 = rf(id)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewDataProvider creates a new instance of DataProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDataProvider(t interface {
mock.TestingT
Cleanup(func())
}) *DataProvider {
mock := &DataProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
With the mock generated, let’s use it to write test cases for ConsumeData which has an external dependency on DataProvider.
// data_consumer_test.go
package test
import (
"EvenOdd/mocks"
"EvenOdd/service"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
// TestConsumeDataForSingleInput tests the ConsumeData function with a single input.
func TestConsumeDataForSingleInput(t *testing.T) {
// Create an instance of the mock
mock := &mocks.DataProvider{}
// Set the expected return value for the GetRandomNumber method
mock.On("GetRandomNumber", 5).Return(3, nil).Once()
// Call ConsumeData that using the mocked DataProvider
result, err := service.ConsumeData(mock, 5)
// Assert that the result and error are as expected
assert.Equal(t, "Odd", result)
assert.NoError(t, err)
// Assert that the GetRandomNumber method was called with the expected input
mock.AssertExpectations(t)
}
// TestConsumeDataForMultipleInputs tests the ConsumeData function with multiple values.
func TestConsumeDataForMultipleInputs(t *testing.T) {
// Create an instance of the mock
mock := &mocks.DataProvider{}
// Set the expected return values for the GetRandomNumber method
mock.On("GetRandomNumber", 20).Return(10, nil).Once()
mock.On("GetRandomNumber", 30).Return(15, nil).Once()
mock.On("GetRandomNumber", 10).Return(5, nil).Once()
// Set the expected return value for an error scenario
mock.On("GetRandomNumber", -1).Return(0, errors.New("Invalid id")).Once()
// Added multiple inputs for testing
testCases := []struct {
input int
want string
err error
}{
{input: 20, want: "Even", err: nil},
{input: 30, want: "Odd", err: nil},
{input: 10, want: "Odd", err: nil},
{input: -1, want: "", err: errors.New("Invalid id")},
}
for _, tc := range testCases {
result, err := service.ConsumeData(mock, tc.input)
// Assert that the result and error are as expected
assert.Equal(t, tc.want, result)
assert.Equal(t, tc.err, err)
}
// Assert that the GetRandomNumber methods were called with the expected inputs
mock.AssertExpectations(t)
}
The first test case is with a single input. Whereas, if we want to write a test using multiple inputs, we can do it in a similar way as the second test case. Also, in second test case, I have added error scenario as well.
To run the test, go inside test package and run command (go test) :
go test
PASS
ok RandomizedEvenOdd/test 0.400s
Voila! There you go!
In this example, the mock is used to simulate the behavior of the DataProvider interface, allowing us to control the output and assert that the interaction with the external dependency is as expected.
By incorporating Mockery, we can easily create maintainable and effective mocks for the interfaces, allowing us to write thorough tests for our Go code.
Code github link can be found in RandomizedEvenOdd.
Hope this helps in getting started with Mockery!