Testing and mocking are essential practices in software development that help ensure code reliability, stability, and correctness. The Go programming language has built-in support for testing, making it easier for developers to write unit tests for their code.
However, testing can be challenging when dealing with external dependencies such as databases, APIs, and other services. That’s where mocking comes in. By simulating these dependencies, developers can test their code in isolation and avoid relying on external services during testing.
In this article, we’ll explore the different mocking methods available in Go. We’ll also dive into the GoMock framework, a powerful tool for generating mock objects in Go.
By the end of this article, you’ll have a solid understanding of the different mocking methods in Go and how to choose the right one for your needs.
Mocking techniques in Go
There are several Go mocking techniques can be used to create mock objects. Let’s look at each of these techniques and see how they work.
Manually creating mock objects
Manually creating mock objects involves creating a struct that implements the same interface as the real object, but with mock data and behavior. This technique is useful when dealing with simple objects that have a small number of methods. For example:
package main
import "errors"
type Database interface {
Get(key string) (string, error)
Set(key, value string) error
}
type MockDatabase struct {
Data map[string]string
}
func (db *MockDatabase) Get(key string) (string, error) {
value, ok := db.Data[key]
if !ok {
return "", errors.New("key not found")
}
return value, nil
}
func (db *MockDatabase) Set(key, value string) error {
db.Data[key] = value
return nil
}
In the example above, we defined a Database
interface with two methods: Get
and Set
. We then created a MockDatabase
struct that implements the same interface, but with a Data
field that stores mock data.
The Get
method returns the value associated with the given key from the Data
field, and the Set
method adds a key-value pair to the Data
field.
Implementing interfaces
Another way to create mock objects is by implementing interfaces directly. This technique is best used when dealing with more complex objects with many methods. See the following example:
package main
type Database interface {
Get(key string) (string, error)
Set(key, value string) error
}
type MockDatabase struct {
GetFunc func(key string) (string, error)
SetFunc func(key, value string) error
}
func (db *MockDatabase) Get(key string) (string, error) {
return db.GetFunc(key)
}
func (db *MockDatabase) Set(key, value string) error {
return db.SetFunc(key, value)
}
Using function callbacks
The final basic technique for mocking in Go involves using function callbacks. This technique is useful when dealing with functions that are not part of an interface, like so:
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func ProcessData(data string, f func(string) string) string {
result := f(data)
// do some processing with result
return result
}
func TestProcessData(t *testing.T) {
mockFunc := func(data string) string {
return "mocked result"
}
result := ProcessData("input data", mockFunc)
assert.Equal(t, "mocked result", result)
}
The example above defines a ProcessData
function that takes in a string and a callback function. The function uses the callback to transform the input data and returns the result.
In our test case, we defined a mock function that always returns a fixed result and passes it into the ProcessData
function. We then used an assertion to ensure that the result returned by ProcessData
matches the expected result.
Using the GoMock framework for mock generation
When writing unit tests for code that depends on external dependencies, such as databases or web services, it can be difficult to ensure that the tests are repeatable and predictable.
One approach to solving this problem is to use mock objects, which simulate the behavior of the external dependencies but with controlled inputs and outputs. Using mocking frameworks like GoMock or Testify Mock can help you create mock objects to use in your tests.
The next sections will explain how to install and use GoMock to create and manage mock objects for unit testing in Go.
Key features of GoMock
GoMock provides several features that make it a powerful tool for writing advanced unit tests in Go. For example, it allows you to define interfaces that represent the behavior you want to mock, automatically generating mock objects based on those interfaces.
GoMock also provides tools for recording the calls made to mock objects, making it easier to verify that your code is behaving as expected.
Additionally, you can use GoMock to set expectations for mock object behavior, such as how often a method should be called and with what arguments.
Installing the GoMock framework
To start using GoMock in your Go project, you will first need to install it. GoMock can be installed using the go get
command in your terminal:
go get github.com/golang/mock/gomock
This command will download and install the GoMock package and its dependencies into your Go workspace.
Once GoMock is installed, you can import it into your Go code using the following import
statement:
import (
"github.com/golang/mock/gomock"
)
Install GoMock command line application
GoMock works as a command line application to generate mocked source code, but you also have to install it in your application to use the generated mock code.
Installing the cli tool:
$ go install github.com/golang/mock/mockgen@v1.6.0
To generate mock implementation of specific code, run the following code in the project root, example:
$ mockgen -source=your_code.go -destination=mocks/your_code.go
Creating a mock object using GoMock
To create a mock object using GoMock, you must first define the interface representing the behavior you want to mock. This interface should contain the method signatures that you want to mock.
For example, let’s say we have a Fetcher
interface that retrieves data from an external API. We want to mock this behavior in our unit tests. Here’s what the Fetcher
interface might look like:
type Fetcher interface {
FetchData() ([]byte, error)
}
To generate the mock implementation of the Fetcher interface:
$ mockgen -source=gomock_interface.go -destination=mocks/gomock_interface.go
To create a mock object for this interface using GoMock, we need to follow these steps:
- Create a new controller using
gomock.NewController()
- Create a mock object using the controller’s
CreateMock
method, passing in the interface you want to mock - Set expectations on the mock object’s behavior using GoMock’s methods
- Call the controller’s
Finish
method to indicate you are done with the mock object
Here’s an example code snippet that illustrates how to create a mock object for our Fetcher
interface using GoMock:
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestFetchData(t *testing.T) {
// Create a new controller
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a mock object for the Fetcher interface
mockFetcher := NewMockFetcher(ctrl)
// Set expectations on the mock object's behavior
mockFetcher.EXPECT().FetchData().Return([]byte("data"), nil)
// Call the code under test
data, err := myFunc(mockFetcher)
// Assert the results
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !bytes.Equal(data, []byte("data")) {
t.Errorf("Unexpected data: %v", data)
}
}
Recording calls to a mock object
GoMock allows you to record the calls made to a mock object and set expectations on the behavior of those calls. This can be useful for verifying that your code is interacting with the mock object correctly.
To record the calls made to a mock object using GoMock, you need to create a new gomock.Call
object for each method call you want to record. You can then use the Do
method of the gomock.Call
object to specify the behavior that should occur when the method is called.
Here’s an example code snippet that illustrates how to record calls to a mock object using GoMock:
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestMyFunction(t *testing.T) {
// Create a new controller
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a mock object for the MyInterface interface
mockObj := NewMockMyInterface(ctrl)
// Record the expected calls to the mock object
call1 := mockObj.EXPECT().MyMethod1()
call2 := mockObj.EXPECT().MyMethod2("arg1", "arg2")
// Set expectations on the behavior of the calls
call1.Return(nil)
call2.Return("result", nil)
// Call the code under test
myFunction(mockObj)
// Verify that the expected calls were made
if err := mockObj.AssertExpectationsWereMet(); err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
In this example, we create a mock object for the MyInterface
interface and then use the EXPECT
method to record the expected calls to the mock object. We then use the Return
method to specify the behavior of each call.
Finally, we call the code under test and use the AssertExpectationsWereMet
method to verify that the expected calls were made. An error would be returned if any expected calls were not made, in which case you could follow best practices for error handling in Go to rectify it.
Mocking interfaces in Go with GoMock
In this part, we are going to learn more details on how to use GoMock to generate mock implementations of interface
s in Go.
Generating mock code
Let’s say we have the following code in your main.go
file
package main
// 👇 an interface acting as API Client
type ApiClient interface {
GetData() string
}
// 👇 a function using the ApiClient interface
func Process(client ApiClient) int {
data := client.GetData()
return len(data)
}
func main() {
}
In the above piece of code we have an interface ApiClient
that is being used by the Process
function. Lets say we want to test the Process
function and we require a mock implementation of ApiClient
when doing so.
To generate mock implementation of ApiClient
run the following code in the project root:
$ mockgen -source=main.go -destination=mocks/main.go
This will generate a mock implementation of the interfaace ApiClient
and write the code into mocks/main.go
file.
We don’t really care about the generated code but here is the generated code to satisfy your curiosity (don’t bother to understand it, just glance over it).
// Code generated by MockGen. DO NOT EDIT.
// Source: main.go
// Package mock_main is a generated GoMock package.
package mock_main
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockApiClient is a mock of ApiClient interface.
type MockApiClient struct {
ctrl *gomock.Controller
recorder *MockApiClientMockRecorder
}
// MockApiClientMockRecorder is the mock recorder for MockApiClient.
type MockApiClientMockRecorder struct {
mock *MockApiClient
}
// NewMockApiClient creates a new mock instance.
func NewMockApiClient(ctrl *gomock.Controller) *MockApiClient {
mock := &MockApiClient{ctrl: ctrl}
mock.recorder = &MockApiClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockApiClient) EXPECT() *MockApiClientMockRecorder {
return m.recorder
}
// GetData mocks base method.
func (m *MockApiClient) GetData() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetData")
ret0, _ := ret[0].(string)
return ret0
}
// GetData indicates an expected call of GetData.
func (mr *MockApiClientMockRecorder) GetData() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetData", reflect.TypeOf((*MockApiClient)(nil).GetData))
}
Look closely we have an struct called MockApiClient
that has all the functions of ApiClient
(GetData
in this case). Additionally we have a function called NewMockApiClient
that we can use to get an instance of MockApiClient
in our tests.
Using the mock code
Create a new file called mock_test.go
and add the following code:
package main
import (
"github.com/golang/mock/gomock"
mock_main "github.com/gurleensethi/playground/mocks"
"testing"
)
func TestProcess(t *testing.T) {
ctrl := gomock.NewController(t)
// 👇 create new mock client
mockApiClient := mock_main.NewMockApiClient(ctrl)
// 👇 configure our mock `GetData` function to return mock data
mockApiClient.EXPECT().GetData().Return("Hello World")
length := Process(mockApiClient)
if length != 11 {
t.Fatalf("want: %d, got: %d\n", 11, length)
}
}
We use the NewMockApiClient
function to get an instance that we can use wherever ApiClient
interface is expected.
mockApiClient := mock_main.NewMockApiClient(ctrl)
Pay attention to how we configure the GetData
function on the mock client to return the data that we want.
mockApiClient.EXPECT().GetData().Return("Hello World")
Check out GoMock documentation it has a lot more options to configure mocks, it has a reflection mode which allows you to generate mocks of interfaces from packages that you have not written yourself and much more.
Conclusion
Go provides various options for mocking in unit tests. You can choose from built-in techniques such as implementing interfaces, more advanced options like the monkey patching approach, or third-party libraries like GoMock or Testify Mock.
Each method has advantages and disadvantages; the choice ultimately depends on your use case, specific needs, and preferences.
For example, GoMock and Testify Mock provide more advanced mocking features like call recording, argument matching, and verification, making them suitable for more complex test cases. However, they also have a steeper learning curve and require additional setup and configuration.
In comparison, as mentioned earlier, manually creating a mock object is useful for simple objects with a small number of methods. However, implementing interfaces directly can be more appropriate for more complex objects with many methods, while function callbacks are best for functions that are not part of an interface.
Regardless of the method you choose, learning to use mocking methods effectively can significantly improve the reliability and maintainability of your unit tests, leading to more robust and stable code.
The complete code for the article is available in this GitHub repo.
Thank your for reading this article 🙏🏻 hopefully you learned something new.