All writingNotes from the workRSS

Writing/Engineering

Mock Testing with Go Mockery

Mocking the interfaces SQLC generates with Mockery, so the repository and HTTP handler of a Go e-commerce API can be tested in isolation.

Published
Reading
7 min read
Topics
Engineering, Go
A gopher testing
Cover · A gopher testingImage: ChatGPT

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.

user.sqlSQL
-- name: CreateUser :oneINSERT 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.

user.sql.goGo · 12 lines marked
const createUser = `-- name: CreateUser :oneINSERT 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.

querier.goGo · 3 lines marked
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:

Install MockeryBash
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:

mockery.yamlYAML
all: truetestonly: falseinpackage: truewith-expecter: truepackages:  github.com/repo/project-name/internal/db:    config:      recursive: true      all: True

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

Generate MocksBash
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.

user_test.goGo · 59 lines marked
package testsimport (	"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:

UserExistsByEmail and UserExistsByPhoneNumberGo
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.

RoleNotFoundGo
	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.

CreateUser_SuccessGo
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.

POST /users - SuccessGo
    // 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:

Run TestsBash
go test -v ./...

All tests should pass successfully and pass!

BashBash
=== 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)PASSProcess 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.

Felix Yeboah wearing headphones, working on a laptop
Accra, Ghana

Written by

Felix Yeboah

Software engineer and designer. Self-taught, based in Accra. Ten years taking products from first sketch to production — React and Remix on the front, Go and Postgres underneath.

Local time
14:02 GMT
Writes about
Go, Remix, the work

(→) Keep reading

More notes from the work.

22 posts
All writing

All writingSubscribe via RSS