Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


We’ll continue our Golang API app from previous article. We’ll apply some separation concerns and writing unit test for it.

Overview

Previously, in this article we’ve set up a web application with some libraries such as Cobra and Viper, together along with a database migration setup as well. In that particular set up, we’ve made the app to utilize Cobra to handle the command line parts, while Viper is responsible for managing configs, environment variables and secrets.

Now let’s deal with another important things: interacting with database and writing unit test for each application part. In some simple web applications, doing all this thing altogether (without separation concerns) in one place is good enough, but when the size of an application grows, it is always a good idea to start thinking about the separation concerns and making it testable. A testable component will be easy to understand, to refactor and to reuse.

Application Structure

The application structure will involve the usage of inversion of control with the help of interface, this will also help with unit testing by using mocks. By utilizing interfaces, we decouple the code between caller and callee. This will make the code unit-testable, because we can just use mocks in place of database/API/IO interaction.

First, we’re going to create a dir and install Gin and Testify (unit test library) framework, don’t forget to include go-sqlmock as well (this is for mocking database call) and mockgen for generating mocks:

mkdir internal/rest
go get -u github.com/gin-gonic/gin
go get -u github.com/stretchr/testify  
go get -u github.com/DATA-DOG/go-sqlmock
go install github.com/golang/mock/mockgen@v1.5.0

This internal/rest directory (rest package) will contain everything related to the restful web server handlers. This will also contain our restful server routing code.

Let’s start with the foundation of the rest package:

package rest

type restServer struct {
}

func NewServer() *restServer {
	return &restServer{}
}

Nothing fancy, just a restServer struct and NewServer() function, we’ll call this function when creating the web server.

Ping

Ping is just a simple endpoint that will response with a simple string, this is usually made for health-checking.

The Route

Here’s the Route() function in the same package. Let’s define a route for /ping and assign its handler. Do note that when using Gin, the handler needs to have *gin.Engine as the parameter.

package rest

import "github.com/gin-gonic/gin"

func (rs *restServer) Route(r *gin.Engine) {
	r.GET("/ping", rs.Ping)
}

The Handler

Still inside the rest package, we’re creating the rs.Ping handler. This handler will do nothing but responding with pong.

package rest

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func (rs *restServer) Ping(c *gin.Context) {
	c.String(http.StatusOK, "pong")
}

Updating the Server Command

Now, let’s take a look at server cmd in cmd/server/cmd.go, let’s update launchRestServer() function. Because we already place the web handler in rest package before, launchRestServer() will be smaller.

func launchRestServer() {
	log.Println("Running the web server")

	router := gin.New()

	rs := rest.NewServer()

	rs.Route(router)

	router.Run(":8080")
}

Let’s test by running the server with go run main.go server, we’ll see that now the web server is provided by Gin.

$ go run main.go server
2021/04/24 17:51:35 Running the web server
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> updated-golang-api/internal/rest.(*restServer).Ping-fm (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080

We can then double-check with by visiting http://localhost:8080/ping.

The Test

We’re not done with this /ping path, we need to test the handler as well. This could be done with this test function, place this in rest folder but name the package rest_test. We’re going to utilize testify library (for assertion) and net/http/httptest package (for http testing).

We’re going to use testify, it provides assertions API for us to use.

package rest_test

import (
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"net/http"
	"net/http/httptest"
	"testing"
	"updated-golang-api/internal/rest"
)

func TestPing(t *testing.T) {
	router := gin.Default()

	rs := rest.NewServer()

	rs.Route(router)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/ping", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)
	assert.Equal(t, "pong", w.Body.String())
}

Run this test file with: go test ./... and make sure test result is ok.

$ go test ./...
?   	updated-golang-api	[no test files]
?   	updated-golang-api/cmd	[no test files]
?   	updated-golang-api/cmd/migration	[no test files]
?   	updated-golang-api/cmd/server	[no test files]
?   	updated-golang-api/config	[no test files]
?   	updated-golang-api/db	[no test files]
ok  	updated-golang-api/internal/rest	0.005s

So far there’s no interface involved because testing /ping path is still straight-forward, no database involved, there’s no need to utilize any mock.

Get Users

Now let’s get to the part where database interaction is needed. We’re going to create a GET endpoint that will return all the users from MySQL table we created in previous part.

This time we have to create several interfaces in order to separate between 3 important distinct component types:

  1. the handler,
  2. the service (reusable components), and
  3. the repository (getting data from DB, API, etc).

Handlers will call the service(s), and each service may call the repository. We’re going to decouple these dependencies by using inversion of control.

The Repo and Its Test

We’ll start from the bottom, the component that doesn’t have dependencies, the repository. We’ve made db migration setup in previous article, we’re going to use it. We simply want to get users data from database. We’re going to map the query result to some specific structs. Let’s create a new package model, and create a new file user.go which contains structs related to users data in MySQL.

GetUsersRepoResult will hold the data from GetUsersRepo method in repository code. While GetUsersRepoParam will hold parameters to be passed to that method.

package model

type GetUsersRepoResult struct {
	ID        int
	FirstName string
	LastName  string
	Email     string
}

type GetUsersRepoParam struct {
	Limit  int
	Offset int
}

Now let’s create the repository code, this should be done inside user package with the file name mysql.go. This file name is only for convention, we want to get the data from MySQL table. If we want to do something with external API, we should name the file api.go instead.

This repository code will be straight-forward, just do query to user table with certain limit and offset (via GetUsersRepoParam struct). In this code, we also create a function, called NewRepo which will receive a *sql.DB variable and return the userRepo object, this will be mostly called by the services.

package user

import (
	"context"
	"database/sql"
	"updated-golang-api/internal/model"
)

type userRepo struct {
	appDB *sql.DB
}

func NewRepo(appDB *sql.DB) *userRepo {
	return &userRepo{
		appDB: appDB,
	}
}

func (ur userRepo) GetUsersRepo(ctx context.Context, param model.GetUsersRepoParam) ([]model.GetUsersRepoResult, error) {
	dbConn := ur.appDB

	query := `SELECT id, firstname, lastname, email FROM user LIMIT ? OFFSET ?`
	rows, err := dbConn.Query(query, param.Limit, param.Offset)
	defer rows.Close()
	if err != nil {
		return []model.GetUsersRepoResult{}, err
	}

	users := make([]model.GetUsersRepoResult, 0)
	for rows.Next() {
		var user model.GetUsersRepoResult

		err = rows.Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email)
		users = append(users, user)
	}

	return users, nil
}

Now let’s make the test, we will utilize go-sqlmock library. Because our repository code is already isolated, we can easily create the unit test.

Let’s create mysql_test.go in user directory, but with user_test as the package name. We’ll name our test function: TestUserRepo_GetUsersRepo, this means, this test function will test GetUsersRepo under user package.

Instead of opening the real database connection, we will just get *sql.DB from sqlmock.New(), this will be used for user repo that we want to test: userRepo := user.NewRepo(db).

We will try to mock some rows by using NewRows() assign them to a new variable: rows, just pretend that these rows are the real result of the query (because that’s the goal of the mocks) without really hitting the mysql database.

	rows := mock.NewRows([]string{"id", "firstname", "lastname", "email"}).
		AddRow(1, "john", "doe", "test@email.com").
		AddRow(2, "jane", "doe", "test2@email.com")

We then expect query := SELECT id, firstname, lastname, email FROM user LIMIT ? OFFSET ?`` to be called with args of 10 and 10 (which are the limit and the offset) and return rows above.

We then call the method:

	results, err := userRepo.GetUsersRepo(context.Background(), model.GetUsersRepoParam{
		Limit:  10,
		Offset: 10,
	})

From these setups, we expect that repository code correctly call the query with limit and offset that we specify in the parameters, plus we expect it to correctly return the results (in the form of GetUsersRepoResult struct).

package user_test

import (
	"context"
	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
	"regexp"
	"testing"
	"updated-golang-api/internal/model"
	"updated-golang-api/internal/user"
)

func TestUserRepo_GetUsersRepo(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		panic(err)
	}

	rows := mock.NewRows([]string{"id", "firstname", "lastname", "email"}).
		AddRow(1, "john", "doe", "test@email.com").
		AddRow(2, "jane", "doe", "test2@email.com")

	query := `SELECT id, firstname, lastname, email FROM user LIMIT ? OFFSET ?`

	mock.ExpectQuery(regexp.QuoteMeta(query)).
		WithArgs(10, 10).WillReturnRows(rows)

	userRepo := user.NewRepo(db)

	results, err := userRepo.GetUsersRepo(context.Background(), model.GetUsersRepoParam{
		Limit:  10,
		Offset: 10,
	})
	if err != nil {
		panic(err)
	}

	expectedResults := []model.GetUsersRepoResult{
		{
			ID:        1,
			FirstName: "john",
			LastName:  "doe",
			Email:     "test@email.com",
		},
		{
			ID:        2,
			FirstName: "jane",
			LastName:  "doe",
			Email:     "test2@email.com",
		},
	}

	assert.Equal(t, expectedResults, results)
}

The Service and Its Test

From the repository code, we’ll continue to service code. Service code is called by the rest handler, it also needs to be decoupled (to repository and to handler) by using inversion of control. First, let’s create the user.go under the user package. Because this service code depends on the repository, we’ll need to declare it here.

type UserRepository interface {
	GetUsersRepo(ctx context.Context, param model.GetUsersRepoParam) ([]model.GetUsersRepoResult, error)
}

type UserService struct {
    userRepository UserRepository
}

And because this service is needed by the rest handler, we’ll expose a new function for rest handler to use. This also means that at the top stack trace, we’ll need to instantiate the repository first then assign it to this service.

func NewService(userRepository UserRepository) *UserService {
	return &UserService{
		userRepository: userRepository,
	}
}

Before we go to the service method, we’re going to write the struct models needed, let’s add this in model/user.go file.

type GetUsersServiceResult struct {
	ID        int
	FirstName string
	LastName  string
	Email     string
}

type GetUsersServiceParam struct {
	Limit  int
	Offset int
}

Wait, isn’t it the same with GetUsersRepoResult and GetUsersRepoParam structs we made before? Yes, that’s true, we create other structs for service code too, because we don’t want to reuse the structs, so that both components (repository and service) are flexible, change in one struct won’t affect both components.

Here’s the method, to get user data, we call the repository by using us.userRepository.GetUsersRepo.

func (us UserService) GetUsers(ctx context.Context, param model.GetUsersServiceParam) ([]model.GetUsersServiceResult, error) {
	usersRepo, err := us.userRepository.GetUsersRepo(ctx, model.GetUsersRepoParam{
		Limit:  param.Limit,
		Offset: param.Offset,
	})
	if err != nil {
		return []model.GetUsersServiceResult{}, err
	}

	result := make([]model.GetUsersServiceResult, 0)
	for _, userRepo := range usersRepo {
		result = append(result, model.GetUsersServiceResult{
			ID:        userRepo.ID,
			FirstName: userRepo.FirstName,
			LastName:  userRepo.LastName,
			Email:     userRepo.Email,
		})
	}

	return result, nil
}

It’s time to write the test for this service code. Let’s create user_test.go in user package. Before we write any service code test, we need to remember that service code above depends on the repository code, but we don’t want to really hit the repository code (we don’t want to hit the database). Mock is needed here in place of repository code.

Add this just above the interface definition, don’t forget to create mock directory inside user directory.

This definition is just a comment, but some text editors can pick it up, and we can run the command from inside the text editors.

//go:generate mockgen -source=./user.go -destination=mock/repository.go -package=usermock
type UserRepository interface {
	GetUsersRepo(ctx context.Context, param model.GetUsersRepoParam) ([]model.GetUsersRepoResult, error)
}

// ... rest of the code

We can also run this command in the terminal, the result will be the same.

go generate -run "mockgen -source=./user.go -destination=mock/repository.go -package=usermock"

Mocks will then be generated inside user/mock directory with usermock as its package name. Now let’s create user_test.go inside user directory. This time test function will be named TestUserService_GetUsers.

First, we’re going to create userRepoMock, this is a mock that will replace real user repository.

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	userRepoMock := usermock.NewMockUserRepository(ctrl)

	us := user.NewService(userRepoMock)

We then expect the service to call user repo’s GetUsersRepo with userRepoMock.EXPECT().GetUsersRepo, we also expect the parameters and the results. For the expectation, the first param we pass is gomock.Any(), this means that we don’t care about the parameter we pass there (it’s a context, btw). What we want to test is whether the limit, and the offset are correctly called on repository code.

After the mock expectation, we then call the method we test, get the results and assert them with our expectation. We assert that data we mock and returned from the repository are correctly returned by the service.

package user_test

import (
	"context"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"testing"
	"updated-golang-api/internal/model"
	"updated-golang-api/internal/user"
	usermock "updated-golang-api/internal/user/mock"
)

func TestUserService_GetUsers(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	userRepoMock := usermock.NewMockUserRepository(ctrl)

	us := user.NewService(userRepoMock)

	userRepoMock.EXPECT().GetUsersRepo(gomock.Any(), model.GetUsersRepoParam{
		Limit:  10,
		Offset: 100,
	}).Return([]model.GetUsersRepoResult{
		{
			ID:        1,
			FirstName: "alpha",
			LastName:  "beta",
			Email:     "alpha@email.com",
		},
		{
			ID:        2,
			FirstName: "gamma",
			LastName:  "zona",
			Email:     "gamma@email.com",
		},
	}, nil)

	results, err := us.GetUsers(context.Background(), model.GetUsersServiceParam{
		Limit:  10,
		Offset: 100,
	})
	if err != nil {
		panic(err)
	}

	expected := []model.GetUsersServiceResult{
		{
			ID:        1,
			FirstName: "alpha",
			LastName:  "beta",
			Email:     "alpha@email.com",
		},
		{
			ID:        2,
			FirstName: "gamma",
			LastName:  "zona",
			Email:     "gamma@email.com",
		},
	}

	assert.Equal(t, expected, results)
}

The Handler and Route and Its Test

It’s similar to Ping handler above, which we create a test for, we will also create a test for GetUsers handler. Before that, we need to make sure that we decouple the service from the handler.

Let’s update rest.NewServer function to receive UserService. restServer struct will need to include UserService as well, so that we can call it from our rest handler.

type UserService interface {
	GetUsers(ctx context.Context, param model.GetUsersServiceParam) ([]model.GetUsersServiceResult, error)
}

type restServer struct {
	userService UserService
}

func NewServer(service UserService) *restServer {
	return &restServer{
		userService: service,
	}
}

Now, let’s find a way to serve above GetUsers service for our rest server. We’ll make a handler for it. Let’s create rest/user.go together along with rest/util.go.

Here’s our rest/util.go, it contains a simple function to calculate the offset needed for fetching data to database. We set the limit to 10.

package rest

import (
	"github.com/gin-gonic/gin"
	"strconv"
)

func GetLimitOffset(c *gin.Context) (int, int, int) {
	pageParam, _ := c.GetQuery("page")
	page, _ := strconv.Atoi(pageParam)
	if page == 0 {
		page = 1
	}

	limit := 10
	offset := limit * (page - 1)

	return limit, offset, page
}

Here’s rest/user.go, it calls GetLimitOffset function above, get the limit and offset and send it to service code, which return results data that we’ll marshal as json data.

package rest

import (
	"context"
	"encoding/json"
	"github.com/gin-gonic/gin"
	"updated-golang-api/internal/model"
)

func (rs *restServer) GetUsers(c *gin.Context) {
	limit, offset, _ := GetLimitOffset(c)

	ctx := context.Background()
	users, err := rs.userService.GetUsers(ctx, model.GetUsersServiceParam{
		Limit:  limit,
		Offset: offset,
	})
	if err != nil {
		c.String(500, "unexpected error")
	}

	result, err := json.Marshal(users)
	if err != nil {
		c.String(500, "unexpected error")
	}

	c.String(200, string(result))
}

Don’t forget to add the route:

func (rs *restServer) Route(r *gin.Engine) {
	r.GET("/ping", rs.Ping)
	r.GET("/users", rs.GetUsers) // <== add this
}

Now, let’s create the test for this handler, but before that, let’s generate the mock for UserService interface.

//go:generate mockgen -source=./rest.go -destination=mock/service.go -package=restmock
type UserService interface {
	GetUsers(ctx context.Context, param model.GetUsersServiceParam) ([]model.GetUsersServiceResult, error)
}

// ... rest of the code

Or we can just run this in terminal: go generate -run "mockgen -source=./rest.go -destination=mock/service.go -package=restmock", mocks will be generated in rest/mock/ directory. Everytime we add, update or delete a function, mocks need to be regenerated.

Let’s use the mock in the test, create a new test file rest/user_test.go, with the package name rest_test.

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	userMock := restmock.NewMockUserService(ctrl)

We also mock the call to service code with userServiceMock.EXPECT().GetUsers.

Here’s how it looks like:

package rest_test

import (
	"github.com/gin-gonic/gin"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"

	"net/http"
	"net/http/httptest"
	"testing"
	"updated-golang-api/internal/model"
	"updated-golang-api/internal/rest"
	restmock "updated-golang-api/internal/rest/mock"
)

func TestRestServer_GetUsers(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	userServiceMock := restmock.NewMockUserService(ctrl)

	userServiceMock.EXPECT().GetUsers(gomock.Any(), model.GetUsersServiceParam{
		Limit:  10,
		Offset: 0,
	}).Return([]model.GetUsersServiceResult{
		{
			ID:        1,
			FirstName: "alpha",
			LastName:  "beta",
			Email:     "alpha@email.com",
		},
		{
			ID:        2,
			FirstName: "gamma",
			LastName:  "zona",
			Email:     "gamma@email.com",
		},
	}, nil)

	router := gin.Default()

	rs := rest.NewServer(userServiceMock)

	rs.Route(router)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/users", nil)
	router.ServeHTTP(w, req)

	expectedBody := `[{"ID":1,"FirstName":"alpha","LastName":"beta","Email":"alpha@email.com"},{"ID":2,"FirstName":"gamma","LastName":"zona","Email":"gamma@email.com"}]`

	assert.Equal(t, 200, w.Code)
	assert.Equal(t, expectedBody, w.Body.String())
}

Updating the Server Command

After this, update the server command to create instances for user repo and user service.

First, create database connection from config:

	dbConn, err := database.Conn(config.DBConnectionCfg())

Then, create user repo, send database connection.

	userRepo := user.NewRepo(dbConn)

Then, call user repo’s NewService function with the userRepo parameter.

	userService := user.NewService(userRepo)

Then, call rest’s NewServer function with the userService parameter.

	rs := rest.NewServer(userService)

Here’s how it looks like:

func launchRestServer() {
	log.Println("Running the web server")

	router := gin.New()

	dbConn, err := database.Conn(config.DBConnectionCfg())
	if err != nil {
		log.Panic("cannot open db connection:", err)
	}

	userRepo := user.NewRepo(dbConn)
	userService := user.NewService(userRepo)
	rs := rest.NewServer(userService)

	rs.Route(router)

	router.Run(":8080")
}

Update Regressed Ping Test

Don’t forget to update the Ping test above because we update the rest NewServer function to receive UserService.

// ... rest of the code
ctrl := gomock.NewController(t)
defer ctrl.Finish()
userServiceMock := restmock.NewMockUserService(ctrl)

router := gin.Default()

rs := rest.NewServer(userServiceMock)
// ... rest of the code

After this, we can run all the test files, to make sure that they’re all correct.

$ go test ./...
?   	updated-golang-api	[no test files]
?   	updated-golang-api/cmd	[no test files]
?   	updated-golang-api/cmd/migration	[no test files]
?   	updated-golang-api/cmd/server	[no test files]
?   	updated-golang-api/config	[no test files]
?   	updated-golang-api/db	[no test files]
?   	updated-golang-api/internal/model	[no test files]
ok  	updated-golang-api/internal/rest	0.005s
?   	updated-golang-api/internal/rest/mock	[no test files]
ok  	updated-golang-api/internal/user	(cached)
?   	updated-golang-api/internal/user/mock	[no test files]

Makefile

So far, we’ve made an example of web application that is testable and take some attentions to separation concerns. Now we would like to create a makefile, one of its purpose is making every command available more visible to us.

.PHONY: server

export GO111MODULE=on

test:
	go test ./...

server:
	go run main.go server

migrations-up-all:
	go run main.go migrations 9999

migrations-rollback:
	go run main.go migrations -- -1

Now we can just run make test in order to run all the tests we’ve written, and make server to start the web server.

Let’s Manually Test It

docker run -itd --rm -p 3306:3306 -p 33060:33060 -e MYSQL_ROOT_HOST='%' -e MYSQL_ROOT_PASSWORD='chocolatecake' -e MYSQL_DATABASE=sampleapp --name mysql-8.0.23 mysql/mysql-server:8.0.23
make migrations-up-all
make server

Then insert some data to user table:

INSERT INTO sampleapp.`user` (firstname,lastname,email) VALUES
	 ('test','test2','test@email.com'),
	 ('test3','test4','test2@email.com');

Open new terminal, and run this curl command, if everything is ok, we will see the json result fetched from the database.

$ curl localhost:8080/users
[{"ID":1,"FirstName":"test","LastName":"test2","Email":"test@email.com"},{"ID":2,"FirstName":"test3","LastName":"test4","Email":"test2@email.com"}]