Mock Testing with Go Mockery

Every year I like to take a moment to reflect on the year so far and set some goals for the rest of the year. This year I'm doing it publicly.

I’m on a mission to build an e-commerce API service in Go. One thing I know for sure is that testing is super important in Go development.

Go has a great testing framework built into its standard library, but I’ve had a bit of trouble picking the best testing tool for my project. I’ve tried out gomock, and testify, but none of them seemed quite right.

In my project, I use SQLC, which is a tool that turns SQL queries into fully type-safe and easy-to-use code. Basically, I write SQLC queries, run the sqlc command to generate code that shows type-safe interfaces to the queries I’ve defined. Then, I add the application code that calls the methods generated by SQLC.

Let’s take a look at some code examples to show you how I’m doing it.

-- name: CreateUser :one
INSERT INTO users (
    name,
    email,
    phone_number,
    password,
    role_id,
    is_verified
) VALUES (
  $1, $2, $3, $4, $5, $6
) RETURNING *;

This SQL query is simple and creates a user in the database. SQLC will automatically generate a Go interface for this query, which I’ll use in my application.

const createUser = `-- name: CreateUser :one
INSERT INTO users (
    name,
    email,
    phone_number,
    password,
    role_id,
    is_verified
) VALUES (
  $1, $2, $3, $4, $5, $6
) RETURNING id, name, email, avatar, phone_number, password, is_verified, role_id, created_at, updated_at
`
 
type CreateUserParams struct {
	Name        string    `json:"name"`
	Email       string    `json:"email"`
	PhoneNumber string    `json:"phone_number"`
	Password    string    `json:"password"`
	RoleID      uuid.UUID `json:"role_id"`
	IsVerified  bool      `json:"is_verified"`
}
 
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
	row := q.db.QueryRowContext(ctx, createUser,
		arg.Name,
		arg.Email,
		arg.PhoneNumber,
		arg.Password,
		arg.RoleID,
		arg.IsVerified,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Email,
		&i.Avatar,
		&i.PhoneNumber,
		&i.Password,
		&i.IsVerified,
		&i.RoleID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

Here’s the code for the generated method after running the sqlc generate command. This is the method I’ll be using in my repository to create users.

type Querier interface {
  CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
}

SQLC also creates the above interface for the query, which I plan to use in my repository. But I’m having trouble testing my repository because I can’t mock the SQLC-generated interface. That’s where Go Mockery comes in handy!

Now, lets be more technical.

What is Go Mockery?

Mockery provides the ability to easily generate mocks for Golang interfaces using the stretchr/testify/mock package. It removes the boilerplate coding required to use mocks.

Let's install mockery:

go install github.com/vektra/mockery/v2@v2.44.2

After installing mockery, we need to now create a config file .mockery.yaml in the root of our project:

all: true
testonly: false
inpackage: true
with-expecter: true
packages:
  github.com/repo/project-name/internal/db:
    config:
      recursive: true
      all: True

Run mockery in the root of your project to generate the mocks:

mockery --all

This creates a mocks directory in the root of your project with the generated mocks. It creates a mock for the Querier interface we defined earlier.

Let’s write a Test

Now, let’s write a test for our CreateUser method.

package tests
 
import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"project-repo/internal/db"
	"project-repo/internal/user"
	"project-repo/internal/utils"
	"project-repo/mocks"
	"project-repo/pkg/models"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
 
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
)
 
func TestCreateUser(t *testing.T) {
	mockQuerier := new(mocks.Querier)
	ctx := context.Background()
 
	arg := models.CreateUserWithRoleAndPrivilegesParams{
		Name:        "test",
		Email:       "mail@example.com",
		PhoneNumber: "234567890",
		Password:    "password",
		RoleName:    "admin",
	}
 
	expectedRole := db.GetRoleByNameRow{
		ID:   uuid.New(),
		Name: arg.RoleName,
	}
 
	repo := user.Repository{
		Queries: mockQuerier,
	}
 
	hashedPassword, err := utils.HashPassword(arg.Password)
	assert.NoError(t, err)
 
	expectedUser := db.User{
		ID:          uuid.New(),
		Name:        "test",
		Email:       arg.Email,
		PhoneNumber: arg.PhoneNumber,
		Password:    hashedPassword,
	}
 
	t.Run("UserExistsByEmail", func(t *testing.T) {
		// Mocking GetUserByEmail to return a user
		mockQuerier.On("GetUserByEmail", ctx, arg.Email).Return(expectedUser, nil)
 
		// Check if the user already exists
		existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, existingUser)
 
		// Since the user already exists, the creation process should stop here
		mockQuerier.AssertExpectations(t)
	})
 
	t.Run("UserExistsByPhoneNumber", func(t *testing.T) {
		// Mocking GetUserByPhoneNumber to return a user
		mockQuerier.On("GetUserByPhoneNumber", ctx, arg.PhoneNumber).Return(expectedUser, nil)
 
		// Check if the phone number already exists
		existingUser, err := repo.Queries.GetUserByPhoneNumber(ctx, arg.PhoneNumber)
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, existingUser)
 
		// Since the phone number already exists, the creation process should stop here
		mockQuerier.AssertExpectations(t)
	})
 
	t.Run("RoleNotFound", func(t *testing.T) {
		// Mocking GetRoleByName to return an error
		mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(db.GetRoleByNameRow{}, sql.ErrNoRows)
 
		// Check if the role exists
		role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
		assert.Error(t, err)
		assert.Empty(t, role)
 
		mockQuerier.AssertExpectations(t)
	})
 
	t.Run("CreateUser_Success", func(t *testing.T) {
		// Mocking successful role retrieval
		expectedRole := db.GetRoleByNameRow{
			ID:   uuid.New(),
			Name: arg.RoleName,
		}
		mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(expectedRole, nil)
 
		role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
		if errors.Is(err, sql.ErrNoRows) {
			assert.Empty(t, role)
		}
 
		// Mocking CreateUser
		mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
			Name:        arg.Name,
			Email:       arg.Email,
			PhoneNumber: arg.PhoneNumber,
			Password:    arg.Password,
			RoleID:      expectedRole.ID,
		}).Return(expectedUser, nil)
 
		// Act
		u, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
			Name:        arg.Name,
			Email:       arg.Email,
			PhoneNumber: arg.PhoneNumber,
			Password:    arg.Password,
			RoleID:      expectedRole.ID,
		})
 
		// Assert
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, u)
 
		mockQuerier.AssertExpectations(t)
	})
 
	t.Run("POST /users - Success", func(t *testing.T) {
		// Mocking CreateUser
		mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
			Name:        arg.Name,
			Email:       arg.Email,
			PhoneNumber: arg.PhoneNumber,
			Password:    arg.Password,
			RoleID:      expectedRole.ID,
		}).Return(expectedUser, nil)
 
		// HTTP handler to create user
		handler := func(w http.ResponseWriter, r *http.Request) {
			var req models.CreateUserWithRoleAndPrivilegesParams
			err := json.NewDecoder(r.Body).Decode(&req)
			if err != nil {
				return
			}
 
			// Check if the user already exists
			existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
 
			if existingUser.ID != uuid.Nil {
				http.Error(w, "user already exists", http.StatusBadRequest)
				return
			}
 
			createdUser, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
				Name:        req.Name,
				Email:       req.Email,
				PhoneNumber: req.PhoneNumber,
				Password:    req.Password,
				RoleID:      expectedRole.ID,
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
 
			err = json.NewEncoder(w).Encode(createdUser)
			if err != nil {
				return
			}
		}
 
		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()
 
		// Act
		userJSON, _ := json.Marshal(arg)
		resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewBuffer(userJSON))
 
		// Assert
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, resp.StatusCode)
	})
}

I noticed you’re overwhelmed with these cases. These test cases test the CreateUser method in the repository, while also mocking the Querier interface methods. Oh, and they also test the HTTP handler that creates a user, mocking the CreateUser method again.

Let me break these down for you:

t.Run("UserExistsByEmail", func(t *testing.T) {
		// Mocking GetUserByEmail to return a user
		mockQuerier.On("GetUserByEmail", ctx, arg.Email).Return(expectedUser, nil)
 
		// Check if the user already exists
		existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, existingUser)
 
		// Since the user already exists, the creation process should stop here
		mockQuerier.AssertExpectations(t)
	})
 
t.Run("UserExistsByPhoneNumber", func(t *testing.T) {
		// Mocking GetUserByPhoneNumber to return a user
		mockQuerier.On("GetUserByPhoneNumber", ctx, arg.PhoneNumber).Return(expectedUser, nil)
 
		// Check if the phone number already exists
		existingUser, err := repo.Queries.GetUserByPhoneNumber(ctx, arg.PhoneNumber)
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, existingUser)
 
		// Since the phone number already exists, the creation process should stop here
		mockQuerier.AssertExpectations(t)
	})

This test checks if the user already exists. It mocks the GetUserByEmail and GetUserByPhoneNumber method to return a user. Then, it calls the methods and check if the returned user is the same as the expected user.

	t.Run("RoleNotFound", func(t *testing.T) {
		// Mocking GetRoleByName to return an error
		mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(db.GetRoleByNameRow{}, sql.ErrNoRows)
 
		// Check if the role exists
		role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
		assert.Error(t, err)
		assert.Empty(t, role)
 
		mockQuerier.AssertExpectations(t)
	})

Just like the above test case, this test also checks if the role provided by the user does exists.

t.Run("CreateUser_Success", func(t *testing.T) {
		// Mocking successful role retrieval
		expectedRole := db.GetRoleByNameRow{
			ID:   uuid.New(),
			Name: arg.RoleName,
		}
		mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(expectedRole, nil)
 
		role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
		if errors.Is(err, sql.ErrNoRows) {
			assert.Empty(t, role)
		}
 
		// Mocking CreateUser
		mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
			Name:        arg.Name,
			Email:       arg.Email,
			PhoneNumber: arg.PhoneNumber,
			Password:    arg.Password,
			RoleID:      expectedRole.ID,
		}).Return(expectedUser, nil)
 
		// Act
		u, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
			Name:        arg.Name,
			Email:       arg.Email,
			PhoneNumber: arg.PhoneNumber,
			Password:    arg.Password,
			RoleID:      expectedRole.ID,
		})
 
		// Assert
		assert.NoError(t, err)
		assert.Equal(t, expectedUser, u)
 
		mockQuerier.AssertExpectations(t)
	})

This test case is incredibly interesting. It mocks the successful retrieval of the role, then mocks the CreateUser method to return the expected user. It then calls the CreateUser method and checks if the returned user is the same as the expected user.

    // HTTP handler to create user
		handler := func(w http.ResponseWriter, r *http.Request) {
			var req models.CreateUserWithRoleAndPrivilegesParams
			err := json.NewDecoder(r.Body).Decode(&req)
			if err != nil {
				return
			}
 
			// Check if the user already exists
			existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
 
			if existingUser.ID != uuid.Nil {
				http.Error(w, "user already exists", http.StatusBadRequest)
				return
			}
 
			createdUser, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
				Name:        req.Name,
				Email:       req.Email,
				PhoneNumber: req.PhoneNumber,
				Password:    req.Password,
				RoleID:      expectedRole.ID,
			})
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
 
			err = json.NewEncoder(w).Encode(createdUser)
			if err != nil {
				return
			}
		}
 
		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()
 
		// Act
		userJSON, _ := json.Marshal(arg)
		resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewBuffer(userJSON))
 
		// Assert
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, resp.StatusCode)
	})

With all the test cases in place, we now the handler takes care of incoming POST requests. It checks if the user already exists by querying the database. If they do, it produces an error. If they don’t, it creates a new user and sends back a JSON response. The code also has a test setup using httptest.

Now, you can run the tests:

go test -v ./...

All tests should pass successfully and pass!

=== RUN   TestCreateUser
=== RUN   TestCreateUser/UserExistsByEmail
--- PASS: TestCreateUser/UserExistsByEmail (0.00s)
=== RUN   TestCreateUser/UserExistsByPhoneNumber
--- PASS: TestCreateUser/UserExistsByPhoneNumber (0.00s)
=== RUN   TestCreateUser/RoleNotFound
--- PASS: TestCreateUser/RoleNotFound (0.00s)
=== RUN   TestCreateUser/PrivilegeNotFound
--- PASS: TestCreateUser/PrivilegeNotFound (0.00s)
=== RUN   TestCreateUser/CreateUser_Success
--- PASS: TestCreateUser/CreateUser_Success (0.00s)
=== RUN   TestCreateUser/POST_/users_-_Success
--- PASS: TestCreateUser/POST_/users_-_Success (0.00s)
--- PASS: TestCreateUser (0.07s)
PASS
 
Process finished with the exit code 0

Is this a lot to take in? Yes, it is! But it’s also super important to test your code thoroughly. Mockery makes it easy to mock interfaces and write tests for your Go code.