4 Mocking Approaches for Go

August 13, 2024
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

4 Mocking Approaches for Go

Mocking is an essential aspect of unit testing. It allows you to isolate the code you are testing by replacing dependencies with mock implementations. In doing so, it allows you to simulate the behavior of complex or external dependencies. In Go, there are several ways in which we can mock application behavior.

In this tutorial, we'll take a look at why mocking is important and also take a look at some mocking techniques in Go.

Prerequisites

Before we begin, ensure that you have the following:

  • An understanding of or prior experience with Go
  • Go installed
  • Your preferred text editor or IDE

Why mocking is important

There are four key reasons to use mocking:

  • Isolation: It allows you to test a unit of code separately from its dependencies
  • Deterministic testing: Mocking external services allows you to control the behavior and replies, making testing predictable and repeatable
  • Speed: Mocks are faster than real implementations, therefore, testing takes less time overall
  • Reliability: Mocks prevent flaky tests from failing owing to problems with external services or the network

Set up your project

Open your terminal and create a directory for your project.

mkdir mock_go
cd mock_go

Run the following command to initialize your Go module (and simplify managing dependencies in your project).

go mod init mock_go

Mock with interfaces

In Go, interfaces are a powerful way to abstract dependencies, making it easier to replace them with mocks during testing.

They serve as a contract for the functions a type must implement, along with the parameters which the function accepts, and what the function returns. This allows for greater flexibility and abstraction in how dependencies are handled within a program.

This abstraction facilitates the replacement of actual dependencies with mock implementations during testing, enabling the isolation of the code under test from external systems or services.

Let's say you have a UserService that interacts with a UserRepository to fetch user data, as in the following code example. Create a new file in the project's top-level directory, named main.go, and add the following code to it.

package main

type User struct {
    ID   int
    Name string
}

type UserRepository interface {
    GetUserByID(id int) (*User, error)
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUser(id int) (*User, error) {
    user, err := s.repo.GetUserByID(id)
    if err != nil {
        return nil, err
    }
    return user, nil
}

You can create a mock implementation of the UserRepository interface manually for testing as in this example.

Create a new file named main_test.go and add the following code to it:

package main

import (
    "errors"
    "testing"
)

type MockUserRepository struct {
    users map[int]*User
}

func (m *MockUserRepository) GetUserByID(id int) (*User, error) {
    user, exists := m.users[id]
    if !exists {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func TestUserService_GetUser(t *testing.T) {
    mockRepo := &MockUserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "John Doe"},
        },
    }
    service := NewUserService(mockRepo)

    t.Run("user exists", func(t *testing.T) {
        user, err := service.GetUser(1)
        if err != nil {
            t.Errorf("expected no error, got %v", err)
        }
        if user == nil {
            t.Fatalf("expected user, got nil")
        }
        if user.Name != "John Doe" {
            t.Errorf("expected user name to be 'John Doe', got %v", user.Name)
        }
    })

    t.Run("user does not exist", func(t *testing.T) {
        user, err := service.GetUser(2)
        if err == nil {
            t.Errorf("expected error, got nil")
        }
        if user != nil {
            t.Errorf("expected nil user, got %v", user)
        }
    })
}

The code above tests the functionality of the UserService that interacts with a UserRepository to fetch user data. It creates unit tests for its GetUser() method, which retrieves user information based on a provided user ID.

The tests create scenarios to verify how the GetUser() method behaves. The first scenario tests when the requested user exists in the system. The second scenario tests when the requested user does not exist. To facilitate these tests, a mock implementation of the UserRepository interface is utilized, allowing controlled simulation of user data retrieval operations.

Run the following command to run the test suite:

go test -v

You should see the test result in your terminal, similar to the screenshot below.

Result showing mocking with interfaces

Use a mocking library

Manually mocking in Go can be tedious, especially for large interfaces. However, mocking libraries like Go Mock can automate this process.

Like other mocking libraries, Go Mock is designed to simplify the process of creating mock objects for unit testing. It automates the creation of these objects, which can significantly speed up the testing process and reduce the amount of boilerplate code needed.

Go Mock, specifically, offers several advantages that make it a valuable tool for automating the creation of mock objects:

  • Does not override your modules: This means it doesn't alter the original modules you're testing, thus preserving their integrity and functionality outside of the testing environment.
  • Does not require passing modules as function parameters: This simplifies the process of setting up tests by eliminating unnecessary parameters.
  • Does not require creating callbacks or wrappers around libraries: This further streamlines the testing process by avoiding the need for additional setup steps.
  • Provides a succinct API with a human-readable DSL: This makes it easier to define all possible object operations and interactions, enhancing the readability and maintainability of test code.

Let's go through an example demonstrating how to use Go Mock to generate a mock for the UserRepository interface.

Install the required dependencies

Install Go Mock, the mockgen tool, and stretchr/testify package. The mockgen package is used to generate mock code. It works by taking an interface defined in your code as input and generates a corresponding mock object.

In our case, the mockgen package will be used to generate the MockUserRepository struct and its methods. The stretchr/testify is used to enhance the test assertions. In our case, it does that by checking if the UserService functions behave as expected.

Run the following commands:

go get go.uber.org/mock/gomock  
go install go.uber.org/mock/mockgen@latest
go get github.com/stretchr/testify/assert

Define the UserRepository interface

Here, you’ll need to create the repository package and define the UserRepository interface. First, create a new directory named repository:

mkdir repository

Then, create a new file named user_repository.go inside the repository directory. After that, in repository/user_repository.go, paste the code below:

package repository

type User struct {
    ID    int
    Name  string
    Email string
}

type UserRepository interface {
    GetUserByID(id int) (*User, error)
    CreateUser(user *User) error
}

Generate the mock

Now, you’ll use the mockgen tool to generate mocks for your interface by running the following command in your terminal:

mockgen -source=repository/user_repository.go -package=repository > repository/mock_user_repository.go

Define the UserService

Next, you’ll need to create the service package and define the UserService struct inside it. So, first, create a new directory named service:

mkdir service

Then, create a new file named user_service.go inside the service directory, and paste the code below into it.

package service

import "mock_go/repository"

type UserService struct {
    repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUserByID(id int) (*repository.User, error) {
    return s.repo.GetUserByID(id)
}

func (s *UserService) CreateUser(user *repository.User) error {
    return s.repo.CreateUser(user)
}

You’ll need to change up some things in the generated code. Replace it with the following code:

package repository

import (
   reflect "reflect"

   gomock "go.uber.org/mock/gomock"
)

// MockUserRepository is a mock of UserRepository interface.
type MockUserRepository struct {
   ctrl     *gomock.Controller
   recorder *MockUserRepositoryMockRecorder
}

// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository.
type MockUserRepositoryMockRecorder struct {
   mock *MockUserRepository
}

// NewMockUserRepository creates a new mock instance.
func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
   mock := &MockUserRepository{ctrl: ctrl}
   mock.recorder = &MockUserRepositoryMockRecorder{mock}
   return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
   return m.recorder
}

// CreateUser mocks base method.
func (m *MockUserRepository) CreateUser(user *User) error {
   m.ctrl.T.Helper()
   ret := m.ctrl.Call(m, "CreateUser", user)
   ret0, _ := ret[0].(error)
   return ret0
}

// CreateUser indicates an expected call of CreateUser.
func (mr *MockUserRepositoryMockRecorder) CreateUser(user any) *gomock.Call {
   mr.mock.ctrl.T.Helper()
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUserRepository)(nil).CreateUser), user)
}

// GetUserByID mocks base method.
func (m *MockUserRepository) GetUserByID(id int) (*User, error) {
   m.ctrl.T.Helper()
   ret := m.ctrl.Call(m, "GetUserByID", id)
   ret0, _ := ret[0].(*User)
   ret1, _ := ret[1].(error)
   return ret0, ret1
}

// GetUserByID indicates an expected call of GetUserByID.
func (mr *MockUserRepositoryMockRecorder) GetUserByID(id any) *gomock.Call {
   mr.mock.ctrl.T.Helper()
   return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockUserRepository)(nil).GetUserByID), id)
}

Write unit tests using the mock

To write the unit test using mocks, you’ll need a test file to test UserService. So, create a new file named user_service_test.go inside the service directory. Then paste the code below into the new file:

package service

import (
    "testing"

    "mock_go/repository"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"
)

func TestGetUserByID(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := repository.NewMockUserRepository(ctrl)
    userService := NewUserService(mockRepo)

    user := &repository.User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    mockRepo.EXPECT().GetUserByID(1).Return(user, nil)

    result, err := userService.GetUserByID(1)
    assert.NoError(t, err)
    assert.Equal(t, user, result)
}

func TestCreateUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := repository.NewMockUserRepository(ctrl)
    userService := NewUserService(mockRepo)

    user := &repository.User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    mockRepo.EXPECT().CreateUser(user).Return(nil)

    err := userService.CreateUser(user)
    assert.NoError(t, err)
}

Run the tests

Now, run the tests using the command below:

go test -v ./...

You should see output in your terminal similar to the screenshot below.

?  	 mock_go/repository    [no test files]
=== RUN   TestUserService_GetUser
=== RUN   TestUserService_GetUser/user_exists
=== RUN   TestUserService_GetUser/user_does_not_exist
--- PASS: TestUserService_GetUser (0.00s)
	--- PASS: TestUserService_GetUser/user_exists (0.00s)
	--- PASS: TestUserService_GetUser/user_does_not_exist (0.00s)
PASS
ok 	 mock_go    0.378s
=== RUN   TestGetUserByID
--- PASS: TestGetUserByID (0.00s)
=== RUN   TestCreateUser
--- PASS: TestCreateUser (0.00s)
PASS
ok 	 mock_go/service    0.668s

In this example, you can see how Go Mock simplifies the process of creating and using mock objects for unit testing in Go, which, in turn, makes your tests easier to write and maintain.

Use function callbacks

Another technique for mocking in Go is using function callbacks. This technique allows you to inject custom behavior into your code during testing, which can be particularly useful for mocking functions that do not belong to an interface.

Let’s consider a scenario where UserService depends on a function to fetch user data. Update main.go to match the code below.

package main

type User struct {
   ID   int
   Name string
}

type FetchUserFunc func(id int) (*User, error)

type UserService struct {
   fetchUser FetchUserFunc
}

func NewUserService(fetchUser FetchUserFunc) *UserService {
   return &UserService{fetchUser: fetchUser}
}

func (s *UserService) GetUser(id int) (*User, error) {
   user, err := s.fetchUser(id)
   if err != nil {
       return nil, err
   }
   return user, nil
}

Now, update main_test.go to match the code below. You can use function callbacks to mock the fetchUser() function during testing:

package main

import (
   "errors"
   "testing"

   "github.com/stretchr/testify/assert"
)

func TestUserService_GetUser_WithCallback(t *testing.T) {
   mockFetchUser := func(id int) (*User, error) {
       if id == 1 {
           return &User{ID: 1, Name: "John Doe"}, nil
       }
       return nil, errors.New("user not found")
   }
   service := NewUserService(mockFetchUser)

   t.Run("user exists", func(t *testing.T) {
       user, err := service.GetUser(1)
       assert.NoError(t, err)
       assert.NotNil(t, user)
       assert.Equal(t, "John Doe", user.Name)
   })

   t.Run("user does not exist", func(t *testing.T) {
       user, err := service.GetUser(2)
       assert.Error(t, err)
       assert.Nil(t, user)
   })
}

In this example, the fetchUser() function is replaced with a custom implementation during testing, allowing you to control the behavior of the GetUser() method.

If you run the tests again, you should see output similar to the following for main_test.go:

?  	 mock_go/repository    [no test files]
=== RUN   TestUserService_GetUser_WithCallback
=== RUN   TestUserService_GetUser_WithCallback/user_exists
=== RUN   TestUserService_GetUser_WithCallback/user_does_not_exist
--- PASS: TestUserService_GetUser_WithCallback (0.00s)
	--- PASS: TestUserService_GetUser_WithCallback/user_exists (0.00s)
	--- PASS: TestUserService_GetUser_WithCallback/user_does_not_exist (0.00s)

Mock HTTP requests

In addition to interfaces, you often need to mock HTTP requests in Go, especially when developing unit tests for functions that make outbound HTTP calls. This practice allows you to simulate the behavior of external services without actually making network requests, thereby speeding up tests and isolating them from external dependencies.

The net/http/httptest package is useful for this purpose. It provides functionality such as creating a mock HTTP server and generating mock HTTP requests. These features allow you to simulate various scenarios, including different HTTP methods, headers, and body content, and to assert the outcomes of those requests.

With `httptest` you can test your application under different conditions such as handling errors, parsing responses, and interacting with APIs.

Let’s say that UserService fetches user data from an external API. Update your main.go file with the following:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type UserService struct {
    apiURL string
}

func NewUserService(apiURL string) *UserService {
    return &UserService{apiURL: apiURL}
}

func (s *UserService) GetUser(id int) (*User, error) {
    url := fmt.Sprintf("%s/users/%d", s.apiURL, id)
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("failed to fetch user: %s", resp.Status)
    }

    var user User
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return nil, err
    }

    return &user, nil
}

You can use the httptest package to mock the HTTP server. Update your main_test.go file with the following:

package main

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestUserService_GetUser(t *testing.T) {
    t.Run("user exists", func(t *testing.T) {
        user := &User{ID: 1, Name: "John Doe"}
        server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            json.NewEncoder(w).Encode(user)
        }))
        defer server.Close()

        service := NewUserService(server.URL)
        result, err := service.GetUser(1)
        assert.NoError(t, err)
        assert.NotNil(t, result)
        assert.Equal(t, user.Name, result.Name)
    })

    t.Run("user does not exist", func(t *testing.T) {
        server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            http.Error(w, "user not found", http.StatusNotFound)
        }))
        defer server.Close()

        service := NewUserService(server.URL)
        result, err := service.GetUser(1)
        assert.Error(t, err)
        assert.Nil(t, result)
    })
}

If you run the tests again, one final time, you should see output similar to the following for main_test.go:

?  	 mock_go/repository    [no test files]
=== RUN   TestUserService_GetUser
=== RUN   TestUserService_GetUser/user_exists
=== RUN   TestUserService_GetUser/user_does_not_exist
--- PASS: TestUserService_GetUser (0.00s)
	--- PASS: TestUserService_GetUser/user_exists (0.00s)
	--- PASS: TestUserService_GetUser/user_does_not_exist (0.00s)

In this example, httptest.NewServer() creates a mock server that responds with predetermined responses. This allows you to test how your UserService handles different HTTP responses without making real network calls.

Those are 4 ways to mock in Go

In this article, we examined why mocking is important and some Golang mocking techniques. Mocking is a vital technique in unit testing that isolates the code under test by replacing dependencies with mock implementations.

Learning how to write more effective unit tests not only ensures that your code is maintainable but also helps you stand out as a developer.

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.

The file icon in the post's main image was created by Freepik on Flaticon.