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.2After 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: TrueRun mockery in the root of your project to generate the mocks:
mockery --allThis 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 0Is 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.