@sudocode_

Mock Testing with Go Mockery

August 23, 2024 / 9 min read

Last Updated: August 23, 2024

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

1
-- name: CreateUser :one
2
INSERT INTO users (
3
name,
4
email,
5
phone_number,
6
password,
7
role_id,
8
is_verified
9
) VALUES (
10
$1, $2, $3, $4, $5, $6
11
) 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.go

1
const createUser = `-- name: CreateUser :one
2
INSERT INTO users (
3
name,
4
email,
5
phone_number,
6
password,
7
role_id,
8
is_verified
9
) VALUES (
10
$1, $2, $3, $4, $5, $6
11
) RETURNING id, name, email, avatar, phone_number, password, is_verified, role_id, created_at, updated_at
12
`
13
14
type CreateUserParams struct {
15
Name string `json:"name"`
16
Email string `json:"email"`
17
PhoneNumber string `json:"phone_number"`
18
Password string `json:"password"`
19
RoleID uuid.UUID `json:"role_id"`
20
IsVerified bool `json:"is_verified"`
21
}
22
23
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
24
row := q.db.QueryRowContext(ctx, createUser,
25
arg.Name,
26
arg.Email,
27
arg.PhoneNumber,
28
arg.Password,
29
arg.RoleID,
30
arg.IsVerified,
31
)
32
var i User
33
err := row.Scan(
34
&i.ID,
35
&i.Name,
36
&i.Email,
37
&i.Avatar,
38
&i.PhoneNumber,
39
&i.Password,
40
&i.IsVerified,
41
&i.RoleID,
42
&i.CreatedAt,
43
&i.UpdatedAt,
44
)
45
return i, err
46
}

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.

You should know!

Before I continue, I’m using the repository pattern in my project. This pattern keeps the data access and business logic separate. It’s a great way to make the code more modular and easier to test.

querier.go

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

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 Mockery

1
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.yaml

1
all: true
2
testonly: false
3
inpackage: true
4
with-expecter: true
5
packages:
6
github.com/repo/project-name/internal/db:
7
config:
8
recursive: true
9
all: True

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

Generate Mocks

1
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.go

1
package tests
2
3
import (
4
"bytes"
5
"context"
6
"database/sql"
7
"encoding/json"
8
"errors"
9
"project-repo/internal/db"
10
"project-repo/internal/user"
11
"project-repo/internal/utils"
12
"project-repo/mocks"
13
"project-repo/pkg/models"
14
"net/http"
15
"net/http/httptest"
16
"testing"
17
"time"
18
19
"github.com/google/uuid"
20
"github.com/stretchr/testify/assert"
21
)
22
23
func TestCreateUser(t *testing.T) {
24
mockQuerier := new(mocks.Querier)
25
ctx := context.Background()
26
27
arg := models.CreateUserWithRoleAndPrivilegesParams{
28
Name: "test",
29
Email: "mail@example.com",
30
PhoneNumber: "234567890",
31
Password: "password",
32
RoleName: "admin",
33
}
34
35
expectedRole := db.GetRoleByNameRow{
36
ID: uuid.New(),
37
Name: arg.RoleName,
38
}
39
40
repo := user.Repository{
41
Queries: mockQuerier,
42
}
43
44
hashedPassword, err := utils.HashPassword(arg.Password)
45
assert.NoError(t, err)
46
47
expectedUser := db.User{
48
ID: uuid.New(),
49
Name: "test",
50
Email: arg.Email,
51
PhoneNumber: arg.PhoneNumber,
52
Password: hashedPassword,
53
}
54
55
t.Run("UserExistsByEmail", func(t *testing.T) {
56
// Mocking GetUserByEmail to return a user
57
mockQuerier.On("GetUserByEmail", ctx, arg.Email).Return(expectedUser, nil)
58
59
// Check if the user already exists
60
existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
61
assert.NoError(t, err)
62
assert.Equal(t, expectedUser, existingUser)
63
64
// Since the user already exists, the creation process should stop here
65
mockQuerier.AssertExpectations(t)
66
})
67
68
t.Run("UserExistsByPhoneNumber", func(t *testing.T) {
69
// Mocking GetUserByPhoneNumber to return a user
70
mockQuerier.On("GetUserByPhoneNumber", ctx, arg.PhoneNumber).Return(expectedUser, nil)
71
72
// Check if the phone number already exists
73
existingUser, err := repo.Queries.GetUserByPhoneNumber(ctx, arg.PhoneNumber)
74
assert.NoError(t, err)
75
assert.Equal(t, expectedUser, existingUser)
76
77
// Since the phone number already exists, the creation process should stop here
78
mockQuerier.AssertExpectations(t)
79
})
80
81
t.Run("RoleNotFound", func(t *testing.T) {
82
// Mocking GetRoleByName to return an error
83
mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(db.GetRoleByNameRow{}, sql.ErrNoRows)
84
85
// Check if the role exists
86
role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
87
assert.Error(t, err)
88
assert.Empty(t, role)
89
90
mockQuerier.AssertExpectations(t)
91
})
92
93
t.Run("CreateUser_Success", func(t *testing.T) {
94
// Mocking successful role retrieval
95
expectedRole := db.GetRoleByNameRow{
96
ID: uuid.New(),
97
Name: arg.RoleName,
98
}
99
mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(expectedRole, nil)
100
101
role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
102
if errors.Is(err, sql.ErrNoRows) {
103
assert.Empty(t, role)
104
}
105
106
// Mocking CreateUser
107
mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
108
Name: arg.Name,
109
Email: arg.Email,
110
PhoneNumber: arg.PhoneNumber,
111
Password: arg.Password,
112
RoleID: expectedRole.ID,
113
}).Return(expectedUser, nil)
114
115
// Act
116
u, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
117
Name: arg.Name,
118
Email: arg.Email,
119
PhoneNumber: arg.PhoneNumber,
120
Password: arg.Password,
121
RoleID: expectedRole.ID,
122
})
123
124
// Assert
125
assert.NoError(t, err)
126
assert.Equal(t, expectedUser, u)
127
128
mockQuerier.AssertExpectations(t)
129
})
130
131
t.Run("POST /users - Success", func(t *testing.T) {
132
// Mocking CreateUser
133
mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
134
Name: arg.Name,
135
Email: arg.Email,
136
PhoneNumber: arg.PhoneNumber,
137
Password: arg.Password,
138
RoleID: expectedRole.ID,
139
}).Return(expectedUser, nil)
140
141
// HTTP handler to create user
142
handler := func(w http.ResponseWriter, r *http.Request) {
143
var req models.CreateUserWithRoleAndPrivilegesParams
144
err := json.NewDecoder(r.Body).Decode(&req)
145
if err != nil {
146
return
147
}
148
149
// Check if the user already exists
150
existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
151
if err != nil {
152
http.Error(w, err.Error(), http.StatusInternalServerError)
153
return
154
}
155
156
if existingUser.ID != uuid.Nil {
157
http.Error(w, "user already exists", http.StatusBadRequest)
158
return
159
}
160
161
createdUser, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
162
Name: req.Name,
163
Email: req.Email,
164
PhoneNumber: req.PhoneNumber,
165
Password: req.Password,
166
RoleID: expectedRole.ID,
167
})
168
if err != nil {
169
http.Error(w, err.Error(), http.StatusInternalServerError)
170
return
171
}
172
173
err = json.NewEncoder(w).Encode(createdUser)
174
if err != nil {
175
return
176
}
177
}
178
179
server := httptest.NewServer(http.HandlerFunc(handler))
180
defer server.Close()
181
182
// Act
183
userJSON, _ := json.Marshal(arg)
184
resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewBuffer(userJSON))
185
186
// Assert
187
assert.NoError(t, err)
188
assert.Equal(t, http.StatusOK, resp.StatusCode)
189
})
190
}

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 UserExistsByPhoneNumber

1
t.Run("UserExistsByEmail", func(t *testing.T) {
2
// Mocking GetUserByEmail to return a user
3
mockQuerier.On("GetUserByEmail", ctx, arg.Email).Return(expectedUser, nil)
4
5
// Check if the user already exists
6
existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
7
assert.NoError(t, err)
8
assert.Equal(t, expectedUser, existingUser)
9
10
// Since the user already exists, the creation process should stop here
11
mockQuerier.AssertExpectations(t)
12
})
13
14
t.Run("UserExistsByPhoneNumber", func(t *testing.T) {
15
// Mocking GetUserByPhoneNumber to return a user
16
mockQuerier.On("GetUserByPhoneNumber", ctx, arg.PhoneNumber).Return(expectedUser, nil)
17
18
// Check if the phone number already exists
19
existingUser, err := repo.Queries.GetUserByPhoneNumber(ctx, arg.PhoneNumber)
20
assert.NoError(t, err)
21
assert.Equal(t, expectedUser, existingUser)
22
23
// Since the phone number already exists, the creation process should stop here
24
mockQuerier.AssertExpectations(t)
25
})

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.

RoleNotFound

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

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

CreateUser_Success

1
t.Run("CreateUser_Success", func(t *testing.T) {
2
// Mocking successful role retrieval
3
expectedRole := db.GetRoleByNameRow{
4
ID: uuid.New(),
5
Name: arg.RoleName,
6
}
7
mockQuerier.On("GetRoleByName", ctx, arg.RoleName).Return(expectedRole, nil)
8
9
role, err := repo.Queries.GetRoleByName(ctx, arg.RoleName)
10
if errors.Is(err, sql.ErrNoRows) {
11
assert.Empty(t, role)
12
}
13
14
// Mocking CreateUser
15
mockQuerier.On("CreateUser", ctx, db.CreateUserParams{
16
Name: arg.Name,
17
Email: arg.Email,
18
PhoneNumber: arg.PhoneNumber,
19
Password: arg.Password,
20
RoleID: expectedRole.ID,
21
}).Return(expectedUser, nil)
22
23
// Act
24
u, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
25
Name: arg.Name,
26
Email: arg.Email,
27
PhoneNumber: arg.PhoneNumber,
28
Password: arg.Password,
29
RoleID: expectedRole.ID,
30
})
31
32
// Assert
33
assert.NoError(t, err)
34
assert.Equal(t, expectedUser, u)
35
36
mockQuerier.AssertExpectations(t)
37
})

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 - Success

1
// HTTP handler to create user
2
handler := func(w http.ResponseWriter, r *http.Request) {
3
var req models.CreateUserWithRoleAndPrivilegesParams
4
err := json.NewDecoder(r.Body).Decode(&req)
5
if err != nil {
6
return
7
}
8
9
// Check if the user already exists
10
existingUser, err := repo.Queries.GetUserByEmail(ctx, arg.Email)
11
if err != nil {
12
http.Error(w, err.Error(), http.StatusInternalServerError)
13
return
14
}
15
16
if existingUser.ID != uuid.Nil {
17
http.Error(w, "user already exists", http.StatusBadRequest)
18
return
19
}
20
21
createdUser, err := repo.Queries.CreateUser(ctx, db.CreateUserParams{
22
Name: req.Name,
23
Email: req.Email,
24
PhoneNumber: req.PhoneNumber,
25
Password: req.Password,
26
RoleID: expectedRole.ID,
27
})
28
if err != nil {
29
http.Error(w, err.Error(), http.StatusInternalServerError)
30
return
31
}
32
33
err = json.NewEncoder(w).Encode(createdUser)
34
if err != nil {
35
return
36
}
37
}
38
39
server := httptest.NewServer(http.HandlerFunc(handler))
40
defer server.Close()
41
42
// Act
43
userJSON, _ := json.Marshal(arg)
44
resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewBuffer(userJSON))
45
46
// Assert
47
assert.NoError(t, err)
48
assert.Equal(t, http.StatusOK, resp.StatusCode)
49
})

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 Tests

1
go test -v ./...

All tests should pass successfully and pass!

1
=== RUN TestCreateUser
2
=== RUN TestCreateUser/UserExistsByEmail
3
--- PASS: TestCreateUser/UserExistsByEmail (0.00s)
4
=== RUN TestCreateUser/UserExistsByPhoneNumber
5
--- PASS: TestCreateUser/UserExistsByPhoneNumber (0.00s)
6
=== RUN TestCreateUser/RoleNotFound
7
--- PASS: TestCreateUser/RoleNotFound (0.00s)
8
=== RUN TestCreateUser/PrivilegeNotFound
9
--- PASS: TestCreateUser/PrivilegeNotFound (0.00s)
10
=== RUN TestCreateUser/CreateUser_Success
11
--- PASS: TestCreateUser/CreateUser_Success (0.00s)
12
=== RUN TestCreateUser/POST_/users_-_Success
13
--- PASS: TestCreateUser/POST_/users_-_Success (0.00s)
14
--- PASS: TestCreateUser (0.07s)
15
PASS
16
17
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.

Liked this article? Share it with a friend on Twitter or contact me let's start a new project. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll always be available to respond to your messages.

Have a wonderful day.

– Felix

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.