0% found this document useful (0 votes)
87 views59 pages

Understanding Unit and Integration Testing in Golang

This document discusses unit and integration testing in Golang applications. It describes unit testing as validating that each unit of an application performs as expected by mocking external dependencies. Integration testing verifies that different application modules work together properly. The document then demonstrates setting up a project structure for a messaging API with unit and integration tests, including creating packages for error handling, domain objects, and database interactions. It shows how to write tests that isolate units by mocking dependencies and how to test integration by interacting with real dependencies.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
87 views59 pages

Understanding Unit and Integration Testing in Golang

This document discusses unit and integration testing in Golang applications. It describes unit testing as validating that each unit of an application performs as expected by mocking external dependencies. Integration testing verifies that different application modules work together properly. The document then demonstrates setting up a project structure for a messaging API with unit and integration tests, including creating packages for error handling, domain objects, and database interactions. It shows how to write tests that isolate units by mocking dependencies and how to test integration by interacting with real dependencies.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 59

Understanding Unit and Integration Testing in Golang.

md 1/17/2023

Understanding Unit and Integration Testing in Golang.

Testing each functionality in an application can not be overemphasized. In this article, we will explore just two
types of software testing: Unit and Integration Testing in a Golang Application.

Introduction
Unit Testing
A unit is the smallest testable part of any software. Unit testing is performed to validate that each unit of an
application performs as expected. If an external dependency(a method or a function) is called in the unit
being tested, such function/method must be mocked, so as to test only the unit of interest. An example of a
unit test: testing a function that saves a user details in the database; for we to achieve unit testing here, we
need to mock the database interaction and test only the “save” function to assert that it behaves as expected.

Unit testing increases confidence in changing/maintaining a codebase, it makes debugging easy, makes your
code more reliable(softwaretestingfundamentals.com).

Integration Testing
Integration tests verify that different modules or services used by your application work well together. For
example, it can be testing the interaction with the database or making sure that microservices work together
as expected(atlassian.com).

Functional Testing
Functional tests focus on the business requirements of an application. They only verify the output of action
and do not check the intermediate states of the system when performing that action.

There is sometimes confusion between integration tests and functional tests as they both require multiple
components to interact with each other. The difference is that an integration test may simply verify that you
can query the database while a functional test would expect to get a specific value from the database as
defined by the product requirements(atlassian.com).

This article will focus on majorly on Unit testing and Integration testing.

I wrote a messaging Crud Restful API to demonstrate how it works.

Let’s Begin
Get the code on github
Step 1: Basic Setup

1 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Go module is used in this project for dependency management. So you can create the root directory from
anywhere on your computer.

Create a directory called “efficient-api”

mkdir efficient-api

Change to the directory

cd efficient-api

and initialize the go modules

go mod init efficient-api

Since we are using a real database in this api, we will need to create a .env file to contain our database secret
variables.

touch .env

USERNAME=root
PASSWORD=
DATABASE=efficient
PORT=3306
HOST=127.0.0.1
DBDRIVER=mysql

# Used during Integration tests


USERNAME_TEST=root
PASSWORD_TEST=
DATABASE_TEST=efficient_test
PORT_TEST=3306
HOST_TEST=127.0.0.1
DBDRIVER_TEST=mysql

The database driver used in this article is MySQL.

At this point, we have the following structure:

efficient-api
├── .env
└── go.mod
2 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

You can define your database schema like this:

CREATE TABLE `messages` (


`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NULL,
`body` VARCHAR(200) NULL,
`created_at` TIMESTAMP NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `title_UNIQUE` (`title` ASC));

You can install the packages we will be using in this API if you don't already have them:

go get github.com/DATA-DOG/go-sqlmock
go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql
go get github.com/joho/godotenv
go get github.com/stretchr/testify

Step 2: Error Handling


We will need to handle any errors gracefully when they occur. Let's create a package that will help us do that.

From the root directory, create the utils directory,

mkdir utils

We will define the error_utils and error_formats packages.

i. error_utils:

This package has methods that handle different kinds of errors we may encounter in the application.

Inside the utils directory, create the error_utils package:

cd utils && mkdir error_utils

Then create the error_utils file:

cd error_utils && touch error_utils.go

With the content:

3 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

package error_utils

import (
"encoding/json"
"net/http"
)

type MessageErr interface {


Message() string
Status() int
Error() string
}

type messageErr struct {


ErrMessage string `json:"message"`
ErrStatus int `json:"status"`
ErrError string `json:"error"`
}

func (e *messageErr) Error() string {


return e.ErrError
}

func (e *messageErr) Message() string {


return e.ErrMessage
}

func (e *messageErr) Status() int {


return e.ErrStatus
}

func NewNotFoundError(message string) MessageErr {


return &messageErr{
ErrMessage: message,
ErrStatus: http.StatusNotFound,
ErrError: "not_found",
}
}

func NewBadRequestError(message string) MessageErr {


return &messageErr{
ErrMessage: message,
ErrStatus: http.StatusBadRequest,
ErrError: "bad_request",
}
}
func NewUnprocessibleEntityError(message string) MessageErr {
return &messageErr{
ErrMessage: message,
ErrStatus: http.StatusUnprocessableEntity,
ErrError: "invalid_request",
}

4 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

func NewApiErrFromBytes(body []byte) (MessageErr, error) {


var result messageErr
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return &result, nil
}

func NewInternalServerError(message string) MessageErr {


return &messageErr{
ErrMessage: message,
ErrStatus: http.StatusInternalServerError,
ErrError: "server_error",
}
}

ii. error_formats

The essence of the package is format nicely database errors that may occur. For instance, The title field in this
article is unique, so if we attempt to insert a value with a title that is already taken, a database error will
surface. So we need to inform the user of the app nicely, rather than him seeing a text that has information of
the database we are using, and so on.

Inside the utils directory, create the error_formats directory, then the error_formats.go file:

mkdir error_formats && touch error_formats.go

package error_formats

import (
"efficient-api/utils/error_utils"
"fmt"
"github.com/go-sql-driver/mysql"
"strings"
)

func ParseError(err error) error_utils.MessageErr {


sqlErr, ok := err.(*mysql.MySQLError)
if !ok {
if strings.Contains(err.Error(), "no rows in result set") {
return error_utils.NewNotFoundError("no record matching given id")
}
return error_utils.NewInternalServerError(fmt.Sprintf("error when trying
to save message: %s", err.Error()))
}
switch sqlErr.Number {
case 1062:

5 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

return error_utils.NewInternalServerError("title already taken")


}
return error_utils.NewInternalServerError(fmt.Sprintf("error when processing
request: %s", err.Error()))
}

Step 3: The Domain(Model)


The domain houses the:

Data transfer object (DTO). See this as the struct that contains the data structure we are using in the
entire application.
Data Access Object(DAO). This is where we perform the logic for retrieving, saving and updating data in
the database.

From the root directory, create the domain directory.

mkdir domain

i. DTO.

Then create the message_dto.go file inside the domain directory:

cd domain && touch message_dto.go

With the content:

package domain

import (
"efficient-api/utils/error_utils"
"strings"
"time"
)

type Message struct {


Id int64 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
}

func (m *Message) Validate() error_utils.MessageErr {


m.Title = strings.TrimSpace(m.Title)
m.Body = strings.TrimSpace(m.Body)
if m.Title == "" {
return error_utils.NewUnprocessibleEntityError("Please enter a valid
6 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

title")
}
if m.Body == "" {
return error_utils.NewUnprocessibleEntityError("Please enter a valid
body")
}
return nil
}

Observe that we defined the method Validate() which is used to validate inputs.

ii. DAO

In the same directory, create the message_dao.go file

touch message_dao.go

With the content:

package domain

import (
"database/sql"
"efficient-api/utils/error_formats"
"efficient-api/utils/error_utils"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)

var (
MessageRepo messageRepoInterface = &messageRepo{}
)

const (
queryGetMessage = "SELECT id, title, body, created_at FROM messages WHERE
id=?;"
queryInsertMessage = "INSERT INTO messages(title, body, created_at) VALUES(?,
?, ?);"
queryUpdateMessage = "UPDATE messages SET title=?, body=? WHERE id=?;"
queryDeleteMessage = "DELETE FROM messages WHERE id=?;"
queryGetAllMessages = "SELECT id, title, body, created_at FROM messages;"
)

type messageRepoInterface interface {


Get(int64) (*Message, error_utils.MessageErr)
Create(*Message) (*Message, error_utils.MessageErr)
Update(*Message) (*Message, error_utils.MessageErr)
Delete(int64) error_utils.MessageErr
GetAll() ([]Message, error_utils.MessageErr)
7 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Initialize(string, string, string, string, string, string) *sql.DB


}
type messageRepo struct {
db *sql.DB
}

func (mr *messageRepo) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost,


DbName string) *sql.DB {
var err error
DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?
charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort,
DbName)

mr.db, err = sql.Open(Dbdriver, DBURL)


if err != nil {
log.Fatal("This is the error connecting to the database:", err)
}
fmt.Printf("We are connected to the %s database", Dbdriver)

return mr.db
}

func NewMessageRepository(db *sql.DB) messageRepoInterface {


return &messageRepo{db: db}
}

func (mr *messageRepo) Get(messageId int64) (*Message, error_utils.MessageErr) {


stmt, err := mr.db.Prepare(queryGetMessage)
if err != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when
trying to prepare message: %s", err.Error()))
}
defer stmt.Close()

var msg Message


result := stmt.QueryRow(messageId)
if getError := result.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt);
getError != nil {
fmt.Println("this is the error man: ", getError)
return nil, error_formats.ParseError(getError)
}
return &msg, nil
}

func (mr *messageRepo) GetAll() ([]Message, error_utils.MessageErr) {


stmt, err := mr.db.Prepare(queryGetAllMessages)
if err != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when
trying to prepare all messages: %s", err.Error()))
}
defer stmt.Close()

rows, err := stmt.Query()


if err != nil {
8 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

return nil, error_formats.ParseError(err)


}
defer rows.Close()

results := make([]Message, 0)

for rows.Next() {
var msg Message
if getError := rows.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt);
getError != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when
trying to get message: %s", getError.Error()))
}
results = append(results, msg)
}
if len(results) == 0 {
return nil, error_utils.NewNotFoundError("no records found")
}
return results, nil
}

func (mr *messageRepo) Create(msg *Message) (*Message, error_utils.MessageErr) {


fmt.Println("WE REACHED THE DOMAIN")
stmt, err := mr.db.Prepare(queryInsertMessage)
if err != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when
trying to prepare user to save: %s", err.Error()))
}
fmt.Println("WE DIDNT REACH HERE")

defer stmt.Close()

insertResult, createErr := stmt.Exec(msg.Title, msg.Body, msg.CreatedAt)


if createErr != nil {
return nil, error_formats.ParseError(createErr)
}
msgId, err := insertResult.LastInsertId()
if err != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when
trying to save message: %s", err.Error()))
}
msg.Id = msgId

return msg, nil


}

func (mr *messageRepo) Update(msg *Message) (*Message, error_utils.MessageErr) {


stmt, err := mr.db.Prepare(queryUpdateMessage)
if err != nil {
return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when
trying to prepare user to update: %s", err.Error()))
}
defer stmt.Close()

9 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

_, updateErr := stmt.Exec(msg.Title, msg.Body, msg.Id)


if updateErr != nil {
return nil, error_formats.ParseError(updateErr)
}
return msg, nil
}

func (mr *messageRepo) Delete(msgId int64) error_utils.MessageErr {


stmt, err := mr.db.Prepare(queryDeleteMessage)
if err != nil {
return error_utils.NewInternalServerError(fmt.Sprintf("error when trying
to delete message: %s", err.Error()))
}
defer stmt.Close()

if _, err := stmt.Exec(msgId); err != nil {


return error_utils.NewInternalServerError(fmt.Sprintf("error when trying
to delete message %s", err.Error()))
}
return nil
}

As seen, a lot is going on in that file. One of the most important things I want you to observe is the use of
Interface. Using interface will enable us to mock the domain methods when we write unit tests for the
services and controllers that will be defined later. The interface is also handy when we need to fake the
database connections in each domain method.

Since this api is pretty basic, I decided not to use an ORM.

Unit Tests for the Domain Methods:

Will be testing the methods defined in the DAO. Observe that each of them have database interaction. For we
to achieve unit tests, we must fake the database connection. From the message_dao.go file, we defined the
*NewMessageRepository(sql.DB) function that implements the messageRepoInterface. This is where the
“magic” happens. We can be able to swap the DB instance with a mock.

We will use go-sqlmock to test database interactions. This is essential to achieve unit test, else the test
becomes an integration test with a real database connection.

Create message_test.go file inside the domain directory:

touch message_test.go

package domain

import (
"errors"
"fmt"
"github.com/DATA-DOG/go-sqlmock"
10 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

"reflect"
"testing"
"time"
)

var created_at = time.Now()

func TestMessageRepo_Get(t *testing.T) {


db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database
connection", err)
}
defer db.Close()
s := NewMessageRepository(db)

tests := []struct {
name string
s messageRepoInterface
msgId int64
mock func()
want *Message
wantErr bool
}{
{
//When everything works as expected
name: "OK",
s: s,
msgId: 1,
mock: func() {
//We added one row
rows := sqlmock.NewRows([]string{"Id", "Title", "Body",
"CreatedAt"}).AddRow(1, "title", "body", created_at)
mock.ExpectPrepare("SELECT (.+) FROM
messages").ExpectQuery().WithArgs(1).WillReturnRows(rows)
},
want: &Message{
Id: 1,
Title: "title",
Body: "body",
CreatedAt: created_at,
},
},
{
//When the role tried to access is not found
name: "Not Found",
s: s,
msgId: 1,
mock: func() {
rows := sqlmock.NewRows([]string{"Id", "Title", "Body",
"CreatedAt"}) //observe that we didnt add any role here
mock.ExpectPrepare("SELECT (.+) FROM
messages").ExpectQuery().WithArgs(1).WillReturnRows(rows)
},
11 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

wantErr: true,
},
{
//When invalid statement is provided, ie the SQL syntax is wrong(in
this case, we provided a wrong database)
name: "Invalid Prepare",
s: s,
msgId: 1,
mock: func() {
rows := sqlmock.NewRows([]string{"Id", "Title", "Body",
"CreatedAt"}).AddRow(1, "title", "body", created_at)
mock.ExpectPrepare("SELECT (.+) FROM
wrong_table").ExpectQuery().WithArgs(1).WillReturnRows(rows)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock()
got, err := tt.s.Get(tt.msgId)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error new = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !reflect.DeepEqual(got, tt.want) {
t.Errorf("Get() = %v, want %v", got, tt.want)
}
})
}
}

func TestMessageRepo_Create(t *testing.T) {


db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database",
err)
}
defer db.Close()
s := NewMessageRepository(db)
tm := time.Now()

tests := []struct {
name string
s messageRepoInterface
request *Message
mock func()
want *Message
wantErr bool
}{
{
name: "OK",
s: s,
request: &Message{
12 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Title: "title",
Body: "body",
CreatedAt: tm,
},
mock: func() {
mock.ExpectPrepare("INSERT INTO
messages").ExpectExec().WithArgs("title", "body",
tm).WillReturnResult(sqlmock.NewResult(1, 1))
},
want: &Message{
Id: 1,
Title: "title",
Body: "body",
CreatedAt: tm,
},
},
{
name: "Empty title",
s: s,
request: &Message{
Title: "",
Body: "body",
CreatedAt: tm,
},
mock: func(){
mock.ExpectPrepare("INSERT INTO
messages").ExpectExec().WithArgs("title", "body",
tm).WillReturnError(errors.New("empty title"))
},
wantErr: true,
},
{
name: "Empty body",
s: s,
request: &Message{
Title: "title",
Body: "",
CreatedAt: tm,
},
mock: func(){
mock.ExpectPrepare("INSERT INTO
messages").ExpectExec().WithArgs("title", "body",
tm).WillReturnError(errors.New("empty body"))
},
wantErr: true,
},
{
name: "Invalid SQL query",
s: s,
request: &Message{
Title: "title",
Body: "body",
CreatedAt: tm,
},
13 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

mock: func(){
//Instead of using "INSERT", we used "INSETER"
mock.ExpectPrepare("INSERT INTO
wrong_table").ExpectExec().WithArgs("title", "body", tm).WillReturnError(
errors.New("invalid sql query"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock()
got, err := tt.s.Create(tt.request)
if (err != nil) != tt.wantErr {
fmt.Println("this is the error message: ", err.Message())
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !reflect.DeepEqual(got, tt.want) {
t.Errorf("Create() = %v, want %v", got, tt.want)
}
})
}
}

func TestMessageRepo_Update(t *testing.T) {


db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database",
err)
}
defer db.Close()
s := NewMessageRepository(db)

tests := []struct {
name string
s messageRepoInterface
request *Message
mock func()
want *Message
wantErr bool
}{
{
name: "OK",
s: s,
request: &Message{
Id: 1,
Title: "update title",
Body: "update body",
},
mock: func() {
mock.ExpectPrepare("UPDATE
messages").ExpectExec().WithArgs("update title", "update body",
1).WillReturnResult(sqlmock.NewResult(0, 1))
14 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

},
want: &Message{
Id: 1,
Title: "update title",
Body: "update body",
},
},
{
name: "Invalid SQL Query",
s: s,
request: &Message{
Id: 1,
Title: "update title",
Body: "update body",
},
mock: func() {
mock.ExpectPrepare("UPDATER
messages").ExpectExec().WithArgs("update title", "update body",
1).WillReturnError(errors.New("error in sql query statement"))
},
wantErr: true,
},
{
name: "Invalid Query Id",
s: s,
request: &Message{
Id: 0,
Title: "update title",
Body: "update body",
},
mock: func() {
mock.ExpectPrepare("UPDATE
messages").ExpectExec().WithArgs("update title", "update body",
0).WillReturnError(errors.New("invalid update id"))
},
wantErr: true,
},
{
name: "Empty Title",
s: s,
request: &Message{
Id: 1,
Title: "",
Body: "update body",
},
mock: func() {
mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("",
"update body", 1).WillReturnError(errors.New("Please enter a valid title"))
},
wantErr: true,
},
{
name: "Empty Body",
s: s,
15 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

request: &Message{
Id: 1,
Title: "update title",
Body: "",
},
mock: func() {
mock.ExpectPrepare("UPDATE
messages").ExpectExec().WithArgs("update title", "",
1).WillReturnError(errors.New("Please enter a valid body"))
},
wantErr: true,
},
{
name: "Update failed",
s: s,
request: &Message{
Id: 1,
Title: "update title",
Body: "update body",
},
mock: func() {
mock.ExpectPrepare("UPDATE
messages").ExpectExec().WithArgs("update title", "update body",
1).WillReturnResult(sqlmock.NewErrorResult(errors.New("Update failed")))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock()
got, err := tt.s.Update(tt.request)
if (err != nil) != tt.wantErr {
fmt.Println("this is the error message: ", err.Message())
t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !reflect.DeepEqual(got, tt.want) {
t.Errorf("Update() = %v, want %v", got, tt.want)
}
})
}
}

func TestMessageRepo_GetAll(t *testing.T) {


db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database
connection", err)
}
defer db.Close()
s := NewMessageRepository(db)

tests := []struct {
16 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

name string
s messageRepoInterface
msgId int64
mock func()
want []Message
wantErr bool
}{
{
//When everything works as expected
name: "OK",
s: s,
mock: func() {
//We added two rows
rows := sqlmock.NewRows([]string{"Id", "Title", "Body",
"CreatedAt"}).AddRow(1, "first title", "first body", created_at).AddRow(2, "second
title", "second body", created_at)
mock.ExpectPrepare("SELECT (.+) FROM
messages").ExpectQuery().WillReturnRows(rows)
},
want: []Message{
{
Id: 1,
Title: "first title",
Body: "first body",
CreatedAt: created_at,
},
{
Id: 2,
Title: "second title",
Body: "second body",
CreatedAt: created_at,
},
},
},
{
name: "Invalid SQL Syntax",
s: s,
mock: func() {
//We added two rows
_ = sqlmock.NewRows([]string{"Id", "Title", "Body",
"CreatedAt"}).AddRow(1, "first title", "first body", created_at).AddRow(2, "second
title", "second body", created_at)
//"SELECTS" is used instead of "SELECT"
mock.ExpectPrepare("SELECTS (.+) FROM
messages").ExpectQuery().WillReturnError(errors.New("Error when trying to prepare
all messages"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock()
got, err := tt.s.GetAll()
17 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

if (err != nil) != tt.wantErr {


t.Errorf("GetAll() error new = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetAll() = %v, want %v", got, tt.want)
}
})
}
}

func TestMessageRepo_Delete(t *testing.T) {


db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database
connection", err)
}
defer db.Close()
s := NewMessageRepository(db)

tests := []struct {
name string
s messageRepoInterface
msgId int64
mock func()
want *Message
wantErr bool
}{
{
//When everything works as expected
name: "OK",
s: s,
msgId: 1,
mock: func() {
mock.ExpectPrepare("DELETE FROM
messages").ExpectExec().WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 1))
},
wantErr: false,
},
{
name: "Invalid Id/Not Found Id",
s: s,
msgId: 1,
mock: func() {
mock.ExpectPrepare("DELETE FROM
messages").ExpectExec().WithArgs(100).WillReturnResult(sqlmock.NewResult(0, 0))
},
wantErr: true,
},
{
name: "Invalid SQL query",
s: s,
msgId: 1,
mock: func() {
18 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

mock.ExpectPrepare("DELETE FROMSSSS
messages").ExpectExec().WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 0))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mock()
err := tt.s.Delete(tt.msgId)
if (err != nil) != tt.wantErr {
t.Errorf("Delete() error new = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

//When the right number of arguments are passed


//This test is just to improve coverage
func TestMessageRepo_Initialize(t *testing.T) {
dbdriver := "mysql"
username := "username"
password := "password"
host := "host"
database := "database"
port := "port"
dbConnect := MessageRepo.Initialize(dbdriver, username, password, port, host,
database)
fmt.Println("this is the pool: ", dbConnect)
}

We used the Table Testing technique to test each method. Observe how we swapped the DB connection with
the help of the NewMessageRepository function. No real database is hit, which thus helped us achieve unit
testing in the domain methods.

Run the test suite:

go test

To run individual test use:

go test --run <test function>E.g:


go test --run TestMessageRepo_Get

Step 4: Services

19 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Remember, we are building an API. At the end of the day, we will have controllers. The flow is: the controllers
are in charge of all HTTP requests, the services handle the main logic in the api, the domain processes the
data passed by the service and return a database response which may be the desired result or an error.

So, the domain methods are called in the services.

From the root directory(path: /efficient-api/) create the services directory:

mkdir services

Then, create the messages_service.go file:

cd services && messages_service.go

package services

import (
"efficient-api/domain"
"efficient-api/utils/error_utils"
"time"
)

var (
MessagesService messageServiceInterface = &messagesService{}
)

type messagesService struct{}

type messageServiceInterface interface {


GetMessage(int64) (*domain.Message, error_utils.MessageErr)
CreateMessage(*domain.Message) (*domain.Message, error_utils.MessageErr)
UpdateMessage(*domain.Message) (*domain.Message, error_utils.MessageErr)
DeleteMessage(int64) error_utils.MessageErr
GetAllMessages() ([]domain.Message, error_utils.MessageErr)
}

func (m *messagesService) GetMessage(msgId int64) (*domain.Message,


error_utils.MessageErr) {
message, err := domain.MessageRepo.Get(msgId)
if err != nil {
return nil, err
}
return message, nil
}

func (m *messagesService) GetAllMessages() ([]domain.Message,


error_utils.MessageErr) {
messages, err := domain.MessageRepo.GetAll()

20 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

if err != nil {
return nil, err
}
return messages, nil
}

func (m *messagesService) CreateMessage(message *domain.Message) (*domain.Message,


error_utils.MessageErr) {
if err := message.Validate(); err != nil {
return nil, err
}
message.CreatedAt = time.Now()
message, err := domain.MessageRepo.Create(message)
if err != nil {
return nil, err
}
return message, nil
}

func (m *messagesService) UpdateMessage(message *domain.Message) (*domain.Message,


error_utils.MessageErr) {

if err := message.Validate(); err != nil {


return nil, err
}
current, err := domain.MessageRepo.Get(message.Id)
if err != nil {
return nil, err
}
current.Title = message.Title
current.Body = message.Body

updateMsg, err := domain.MessageRepo.Update(current)


if err != nil {
return nil, err
}
return updateMsg, nil
}

func (m *messagesService) DeleteMessage(msgId int64) error_utils.MessageErr {


msg, err := domain.MessageRepo.Get(msgId)
if err != nil {
return err
}
deleteErr := domain.MessageRepo.Delete(msg.Id)
if deleteErr != nil {
return deleteErr
}
return nil
}

21 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Using Interface is very vital here, to enable us to fake the service's methods when we are performing unit
testing for our controller functions that will be defined soon.

In the test file, we mocked all the domain methods that are called. With the help of the Interface, we defined
in the domain package.

Create messages_service_test.go file inside the services directory:

touch messages_service_test.go

package services

import (
"database/sql"
"efficient-api/domain"
"efficient-api/utils/error_utils"
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"time"
)

var (
tm = time.Now()
getMessageDomain func(messageId int64) (*domain.Message,
error_utils.MessageErr)
createMessageDomain func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr)
updateMessageDomain func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr)
deleteMessageDomain func(messageId int64) error_utils.MessageErr
getAllMessagesDomain func() ([]domain.Message, error_utils.MessageErr)
)

type getDBMock struct {}

func (m *getDBMock) Get(messageId int64) (*domain.Message, error_utils.MessageErr)


{
return getMessageDomain(messageId)
}
func (m *getDBMock) Create(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return createMessageDomain(msg)
}
func (m *getDBMock) Update(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return updateMessageDomain(msg)
}
func (m *getDBMock) Delete(messageId int64) error_utils.MessageErr {

22 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

return deleteMessageDomain(messageId)
}
func (m *getDBMock) GetAll() ([]domain.Message, error_utils.MessageErr) {
return getAllMessagesDomain()
}
func (m *getDBMock) Initialize(string, string, string, string, string, string)
*sql.DB {
return nil
}

///////////////////////////////////////////////////////////////
// Start of "GetMessage" test cases
///////////////////////////////////////////////////////////////
func TestMessagesService_GetMessage_Success(t *testing.T) {
domain.MessageRepo = &getDBMock{} //this is where we swapped the functionality
getMessageDomain = func(messageId int64) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "the title",
Body: "the body",
CreatedAt: tm,
}, nil
}
msg, err := MessagesService.GetMessage(1)
fmt.Println("this is the message: ", msg)
assert.NotNil(t, msg)
assert.Nil(t, err)
assert.EqualValues(t, 1, msg.Id)
assert.EqualValues(t, "the title", msg.Title)
assert.EqualValues(t, "the body", msg.Body)
assert.EqualValues(t, tm, msg.CreatedAt)
}

//Test the not found functionality


func TestMessagesService_GetMessage_NotFoundID(t *testing.T) {
domain.MessageRepo = &getDBMock{}
//MessagesService = &serviceMock{}

getMessageDomain = func(messageId int64) (*domain.Message,


error_utils.MessageErr) {
return nil, error_utils.NewNotFoundError("the id is not found")
}
msg, err := MessagesService.GetMessage(1)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusNotFound, err.Status())
assert.EqualValues(t, "the id is not found", err.Message())
assert.EqualValues(t, "not_found", err.Error())
}
///////////////////////////////////////////////////////////////
// End of "GetMessage" test cases
///////////////////////////////////////////////////////////////
23 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

///////////////////////////////////////////////////////////////
// Start of "CreateMessage" test cases
///////////////////////////////////////////////////////////////

//Here we call the domain method, so we must mock it


func TestMessagesService_CreateMessage_Success(t *testing.T) {
domain.MessageRepo = &getDBMock{}
createMessageDomain = func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return &domain.Message{
Id: 1,
Title: "the title",
Body: "the body",
CreatedAt: tm,
}, nil
}
request := &domain.Message{
Title: "the title",
Body: "the body",
CreatedAt: tm,
}
msg, err := MessagesService.CreateMessage(request)
fmt.Println("this is the message: ", msg)
assert.NotNil(t, msg)
assert.Nil(t, err)
assert.EqualValues(t, 1, msg.Id)
assert.EqualValues(t, "the title", msg.Title)
assert.EqualValues(t, "the body", msg.Body)
assert.EqualValues(t, tm, msg.CreatedAt)
}

//This is a table test that check both the title and the body
//Since this will never call the domain "Get" method, no need to mock that method
here
func TestMessagesService_CreateMessage_Invalid_Request(t *testing.T) {
tests := []struct {
request *domain.Message
statusCode int
errMsg string
errErr string
}{
{
request: &domain.Message{
Title: "",
Body: "the body",
CreatedAt: tm,
},
statusCode: http.StatusUnprocessableEntity,
errMsg: "Please enter a valid title",
errErr: "invalid_request",
},
{
24 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

request: &domain.Message{
Title: "the title",
Body: "",
CreatedAt: tm,
},
statusCode: http.StatusUnprocessableEntity,
errMsg: "Please enter a valid body",
errErr: "invalid_request",
},
}
for _, tt := range tests {
msg, err := MessagesService.CreateMessage(tt.request)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, tt.errMsg, err.Message())
assert.EqualValues(t, tt.statusCode, err.Status())
assert.EqualValues(t, tt.errErr, err.Error())
}
}

//We mock the "Get" method in the domain here. What could go wrong?,
//Since the title of the message must be unique, an error must be thrown,
//Of course you can also mock when the sql query is wrong, etc(these where covered
in the domain integration__tests),
//For now, we have 100% coverage on the "CreateMessage" method in the service
func TestMessagesService_CreateMessage_Failure(t *testing.T) {
domain.MessageRepo = &getDBMock{}
createMessageDomain = func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return nil, error_utils.NewInternalServerError("title already taken")
}
request := &domain.Message{
Title: "the title",
Body: "the body",
CreatedAt: tm,
}
msg, err := MessagesService.CreateMessage(request)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, "title already taken", err.Message())
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "server_error", err.Error())
}

///////////////////////////////////////////////////////////////
// End of "CreateMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "UpdateMessage" test cases
///////////////////////////////////////////////////////////////
func TestMessagesService_UpdateMessage_Success(t *testing.T) {
25 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

domain.MessageRepo = &getDBMock{}
getMessageDomain = func(messageId int64) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "former title",
Body: "former body",
}, nil
}
updateMessageDomain = func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return &domain.Message{
Id: 1,
Title: "the title update",
Body: "the body update",
}, nil
}
request := &domain.Message{
Title: "the title update",
Body: "the body update",
}
msg, err := MessagesService.UpdateMessage(request)
assert.NotNil(t, msg)
assert.Nil(t, err)
assert.EqualValues(t, 1, msg.Id)
assert.EqualValues(t, "the title update", msg.Title)
assert.EqualValues(t, "the body update", msg.Body)
}

//This is a validation test, it wont call the domain methods, so, we dont need to
mock them.
//It is also a table
func TestMessagesService_UpdateMessage_Empty_Title_Or_Body(t *testing.T) {
tests := []struct {
request *domain.Message
statusCode int
errMsg string
errErr string
}{
{
request: &domain.Message{
Title: "",
Body: "the body",
},
statusCode: http.StatusUnprocessableEntity,
errMsg: "Please enter a valid title",
errErr: "invalid_request",
},
{
request: &domain.Message{
Title: "the title",
Body: "",
},
statusCode: http.StatusUnprocessableEntity,
26 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

errMsg: "Please enter a valid body",


errErr: "invalid_request",
},
}
for _, tt := range tests {
msg, err := MessagesService.UpdateMessage(tt.request)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, tt.statusCode, err.Status())
assert.EqualValues(t, tt.errMsg, err.Message())
assert.EqualValues(t, tt.errErr, err.Error())
}
}

//An error can occur when trying to fetch the user to update, anything from a
timeout error to a not found error.
//We need to test for that.
//Here we checked for 500 error, you can also check for others if you have time.
func TestMessagesService_UpdateMessage_Failure_Getting_Former_Message(t
*testing.T) {
domain.MessageRepo = &getDBMock{}
getMessageDomain = func(messageId int64) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("error getting message")
}
request := &domain.Message{
Title: "the title update",
Body: "the body update",
}
msg, err := MessagesService.UpdateMessage(request)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, "error getting message", err.Message())
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "server_error", err.Error())
}

//We can get the former message, but we might have issues updating it. Here also,
we tested using 500, you can also assert other possible failure status
func TestMessagesService_UpdateMessage_Failure_Updating_Message(t *testing.T) {
domain.MessageRepo = &getDBMock{}

getMessageDomain = func(messageId int64) (*domain.Message,


error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "former title",
Body: "former body",
}, nil
}
updateMessageDomain = func(msg *domain.Message) (*domain.Message,
error_utils.MessageErr){
return nil, error_utils.NewInternalServerError("error updating message")
}
27 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

request := &domain.Message{
Title: "the title update",
Body: "the body update",
}
msg, err := MessagesService.UpdateMessage(request)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.EqualValues(t, "error updating message", err.Message())
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "server_error", err.Error())
}
///////////////////////////////////////////////////////////////
// End of"UpdateMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of"DeleteMessage" test cases
///////////////////////////////////////////////////////////////
func TestMessagesService_DeleteMessage_Success(t *testing.T) {
domain.MessageRepo = &getDBMock{}
getMessageDomain = func(messageId int64) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "former title",
Body: "former body",
}, nil
}
deleteMessageDomain = func(messageId int64) error_utils.MessageErr {
return nil
}
err := MessagesService.DeleteMessage(1)
assert.Nil(t, err)
}

//It can range from a 500 error to a 404 error, we didnt mock deleting the message
because we will not get there
func TestMessagesService_DeleteMessage_Error_Getting_Message(t *testing.T) {
domain.MessageRepo = &getDBMock{}
getMessageDomain = func(messageId int64) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("Something went wrong
getting message")
}
err := MessagesService.DeleteMessage(1)
assert.NotNil(t, err)
assert.EqualValues(t, "Something went wrong getting message", err.Message())
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "server_error", err.Error())
}

func TestMessagesService_DeleteMessage_Error_Deleting_Message(t *testing.T) {


domain.MessageRepo = &getDBMock{}
28 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

getMessageDomain = func(messageId int64) (*domain.Message,


error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "former title",
Body: "former body",
}, nil
}
deleteMessageDomain = func(messageId int64) error_utils.MessageErr {
return error_utils.NewInternalServerError("error deleting message")
}
err := MessagesService.DeleteMessage(1)
assert.NotNil(t, err)
assert.EqualValues(t, "error deleting message", err.Message())
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "server_error", err.Error())
}
///////////////////////////////////////////////////////////////
// End of "DeleteMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "GetAllMessage" test cases
///////////////////////////////////////////////////////////////
func TestMessagesService_GetAllMessages(t *testing.T) {
domain.MessageRepo = &getDBMock{}
getAllMessagesDomain = func() ([]domain.Message, error_utils.MessageErr) {
return []domain.Message{
{
Id: 1,
Title: "first title",
Body: "first body",
},
{
Id: 2,
Title: "second title",
Body: "second body",
},
}, nil
}
messages, err := MessagesService.GetAllMessages()
assert.Nil(t, err)
assert.NotNil(t, messages)
assert.EqualValues(t, messages[0].Id, 1)
assert.EqualValues(t, messages[0].Title, "first title")
assert.EqualValues(t, messages[0].Body, "first body")
assert.EqualValues(t, messages[1].Id, 2)
assert.EqualValues(t, messages[1].Title, "second title")
assert.EqualValues(t, messages[1].Body, "second body")
}

func TestMessagesService_GetAllMessages_Error_Getting_Messages(t *testing.T) {


29 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

domain.MessageRepo = &getDBMock{}
getAllMessagesDomain = func() ([]domain.Message, error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("error getting messages")
}
messages, err := MessagesService.GetAllMessages()
assert.NotNil(t, err)
assert.Nil(t, messages)
assert.EqualValues(t, http.StatusInternalServerError, err.Status())
assert.EqualValues(t, "error getting messages", err.Message())
assert.EqualValues(t, "server_error", err.Error())
}
///////////////////////////////////////////////////////////////
// End of "GetAllMessage" test cases
///////////////////////////////////////////////////////////////

From the test file above, we define the getDBMock struct that is used in place of the actual one. Then in each
of the test cases, we made this fake struct implement the messageServiceInterface in this line:

domain.MessageRepo = &getDBMock{}

Without this line, the actual domain method is called. Take note.

Hence we achieve unit testing in our service methods. How Sweet.

Run the test suite in the services package:

go test

You can also run the test with coverage:

go test -cover

Step 5: Controllers
In the root directory(path: /efficient-api/), create the controllers directory:

mkdir controllers

Then the messages_controller.go file

cd controllers && touch messages_controller.go

30 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

package controllers

import (
"efficient-api/domain"
"efficient-api/services"
"efficient-api/utils/error_utils"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

//Since we are going for the message id more than we, we extracted this
functionality to a function so we can have a DRY code.
func getMessageId(msgIdParam string) (int64, error_utils.MessageErr) {
msgId, msgErr := strconv.ParseInt(msgIdParam, 10, 64)
if msgErr != nil {
return 0, error_utils.NewBadRequestError("message id should be a number")
}
return msgId, nil
}

func GetMessage(c *gin.Context) {


msgId, err := getMessageId(c.Param("message_id"))
if err != nil {
c.JSON(err.Status(), err)
return
}
message, getErr := services.MessagesService.GetMessage(msgId)
if getErr != nil {
c.JSON(getErr.Status(), getErr)
return
}
c.JSON(http.StatusOK, message)
}

func GetAllMessages(c *gin.Context) {


messages, getErr := services.MessagesService.GetAllMessages()
if getErr != nil {
c.JSON(getErr.Status(), getErr)
return
}
c.JSON(http.StatusOK, messages)
}

func CreateMessage(c *gin.Context) {


var message domain.Message
if err := c.ShouldBindJSON(&message); err != nil {
theErr := error_utils.NewUnprocessibleEntityError("invalid json body")
c.JSON(theErr.Status(), theErr)
return
}
msg, err := services.MessagesService.CreateMessage(&message)
if err != nil {
31 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

c.JSON(err.Status(), err)
return
}
c.JSON(http.StatusCreated, msg)
}

func UpdateMessage(c *gin.Context) {


msgId, err := getMessageId(c.Param("message_id"))
if err != nil {
c.JSON(err.Status(), err)
return
}
var message domain.Message
if err := c.ShouldBindJSON(&message); err != nil {
theErr := error_utils.NewUnprocessibleEntityError("invalid json body")
c.JSON(theErr.Status(), theErr)
return
}
message.Id = msgId
msg, err := services.MessagesService.UpdateMessage(&message)
if err != nil {
c.JSON(err.Status(), err)
return
}
c.JSON(http.StatusOK, msg)
}

func DeleteMessage(c *gin.Context) {


msgId, err := getMessageId(c.Param("message_id"))
if err != nil {
c.JSON(err.Status(), err)
return
}
if err := services.MessagesService.DeleteMessage(msgId); err != nil {
c.JSON(err.Status(), err)
return
}
c.JSON(http.StatusOK, map[string]string{"status": "deleted"})
}

From the above, we used the gin-gonic framework. Gin is a cool package for request handling, response
handling, and routing.

Testing the controller functions.

Observe that each controller function called a service method. To achieve unit testing on the controllers, those
service methods must be mocked. We will define a fake struct that implements the messageServiceInterface,
hence allowing us to create the interface method(this time, the way we want it).

Inside the controllers directory, create the messages_controller_test.go file

32 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

touch messages_controller_test.go

package controllers

import (
"bytes"
"efficient-api/domain"
"efficient-api/services"
"efficient-api/utils/error_utils"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)

var (
getMessageService func(msgId int64) (*domain.Message, error_utils.MessageErr)
createMessageService func(message *domain.Message) (*domain.Message,
error_utils.MessageErr)
updateMessageService func(message *domain.Message) (*domain.Message,
error_utils.MessageErr)
deleteMessageService func(msgId int64) error_utils.MessageErr
getAllMessageService func() ([]domain.Message, error_utils.MessageErr)
)

type serviceMock struct {}

func (sm *serviceMock) GetMessage(msgId int64) (*domain.Message,


error_utils.MessageErr) {
return getMessageService(msgId)
}
func (sm *serviceMock) CreateMessage(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return createMessageService(message)
}
func (sm *serviceMock) UpdateMessage(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return updateMessageService(message)
}
func (sm *serviceMock) DeleteMessage(msgId int64) error_utils.MessageErr {
return deleteMessageService(msgId)
}
func (sm *serviceMock) GetAllMessages() ([]domain.Message, error_utils.MessageErr)
{
return getAllMessageService()
}

///////////////////////////////////////////////////////////////
// Start of "GetMessage" test cases
33 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

///////////////////////////////////////////////////////////////
func TestGetMessage_Success(t *testing.T) {
services.MessagesService = &serviceMock{}
getMessageService = func(msgId int64) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "the title",
Body: "the body",
}, nil
}
msgId := "1" //this has to be a string, because is passed through the url
r := gin.Default()
req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil)
rr := httptest.NewRecorder()
r.GET("/messages/:message_id", GetMessage)
r.ServeHTTP(rr, req)

var message domain.Message


err := json.Unmarshal(rr.Body.Bytes(), &message)
assert.Nil(t, err)
assert.NotNil(t, message)
assert.EqualValues(t, http.StatusOK, rr.Code)
assert.EqualValues(t, 1, message.Id)
assert.EqualValues(t, "the title", message.Title)
assert.EqualValues(t, "the body", message.Body)
}

//When an invalid id id passed. No need to mock the service here because we will
never call it
func TestGetMessage_Invalid_Id(t *testing.T) {
msgId := "abc" //this has to be a string, because is passed through the url
r := gin.Default()
req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil)
rr := httptest.NewRecorder()
r.GET("/messages/:message_id", GetMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "message id should be a number", apiErr.Message())
assert.EqualValues(t, "bad_request", apiErr.Error())
}

//We will call the service method here, so we need to mock it


func TestGetMessage_Message_Not_Found(t *testing.T) {
services.MessagesService = &serviceMock{}
getMessageService = func(msgId int64) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewNotFoundError("message not found")
}
msgId := "1" //valid id
34 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

r := gin.Default()
req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil)
rr := httptest.NewRecorder()
r.GET("/messages/:message_id", GetMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusNotFound, apiErr.Status())
assert.EqualValues(t, "message not found", apiErr.Message())
assert.EqualValues(t, "not_found", apiErr.Error())
}

//We will call the service method here, so we need to mock it


//If for any reason, we could not get the message
func TestGetMessage_Message_Database_Error(t *testing.T) {
services.MessagesService = &serviceMock{}
getMessageService = func(msgId int64) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("database error")
}
msgId := "1" //valid id
r := gin.Default()
req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil)
rr := httptest.NewRecorder()
r.GET("/messages/:message_id", GetMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status())
assert.EqualValues(t, "database error", apiErr.Message())
assert.EqualValues(t, "server_error", apiErr.Error())
}
///////////////////////////////////////////////////////////////
// End of "GetMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "CreateMessage" test cases
///////////////////////////////////////////////////////////////
func TestCreateMessage_Success(t *testing.T) {
services.MessagesService = &serviceMock{}
createMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "the title",
Body: "the body",
}, nil
35 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

}
jsonBody := `{"title": "the title", "body": "the body"}`
r := gin.Default()
req, err := http.NewRequest(http.MethodPost, "/messages",
bytes.NewBufferString(jsonBody))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.POST("/messages", CreateMessage)
r.ServeHTTP(rr, req)

var message domain.Message


err = json.Unmarshal(rr.Body.Bytes(), &message)
assert.Nil(t, err)
assert.NotNil(t, message)
assert.EqualValues(t, http.StatusCreated, rr.Code)
assert.EqualValues(t, 1, message.Id)
assert.EqualValues(t, "the title", message.Title)
assert.EqualValues(t, "the body", message.Body)
}

func TestCreateMessage_Invalid_Json(t *testing.T) {


inputJson := `{"title": 1234, "body": "the body"}`
r := gin.Default()
req, err := http.NewRequest(http.MethodPost, "/messages",
bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.POST("/messages", CreateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())

assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "invalid json body", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}

//This test is not really necessary here, because it has been handled in the
service test
func TestCreateMessage_Empty_Body(t *testing.T) {
services.MessagesService = &serviceMock{}
createMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid
body")
}
inputJson := `{"title": "the title", "body": ""}`
r := gin.Default()
36 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

req, err := http.NewRequest(http.MethodPost, "/messages",


bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.POST("/messages", CreateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())

assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "Please enter a valid body", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}
//This test is not really necessary here, because it has been handled in the
service test
func TestCreateMessage_Empty_Title(t *testing.T) {
services.MessagesService = &serviceMock{}
createMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid
title")
}
inputJson := `{"title": "", "body": "the body"}`
r := gin.Default()
req, err := http.NewRequest(http.MethodPost, "/messages",
bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.POST("/messages", CreateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())

assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "Please enter a valid title", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}
///////////////////////////////////////////////////////////////
// End of "CreateMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "UpdateMessage" test cases
///////////////////////////////////////////////////////////////
37 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

func TestUpdateMessage_Success(t *testing.T) {


services.MessagesService = &serviceMock{}
updateMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return &domain.Message{
Id: 1,
Title: "update title",
Body: "update body",
}, nil
}
jsonBody := `{"title": "update title", "body": "update body"}`
r := gin.Default()
id := "1"
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(jsonBody))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)

var message domain.Message


err = json.Unmarshal(rr.Body.Bytes(), &message)
assert.Nil(t, err)
assert.NotNil(t, message)
assert.EqualValues(t, http.StatusOK, rr.Code)
assert.EqualValues(t, 1, message.Id)
assert.EqualValues(t, "update title", message.Title)
assert.EqualValues(t, "update body", message.Body)
}

//We dont need to mock the service method here, because we wont call it
func TestUpdateMessage_Invalid_Id(t *testing.T) {
jsonBody := `{"title": "update title", "body": "update body"}`
r := gin.Default()
id := "abc"
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(jsonBody))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "message id should be a number", apiErr.Message())
assert.EqualValues(t, "bad_request", apiErr.Error())
}

38 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

//When for instance an integer is provided instead of a string


func TestUpdateMessage_Invalid_Json(t *testing.T) {
inputJson := `{"title": 1234, "body": "the body"}`
r := gin.Default()
id := "1"
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())

assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "invalid json body", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}

//This test is not really necessary here, because it has been handled in the
service test
func TestUpdateMessage_Empty_Body(t *testing.T) {
services.MessagesService = &serviceMock{}
updateMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid
body")
}
inputJson := `{"title": "the title", "body": ""}`
id := "1"
r := gin.Default()
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)
apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "Please enter a valid body", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}
//This test is not really necessary here, because it has been handled in the
service test
func TestUpdateMessage_Empty_Title(t *testing.T) {
39 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

services.MessagesService = &serviceMock{}
updateMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid
title")
}
inputJson := `{"title": "", "body": "the body"}`
id := "1"
r := gin.Default()
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(inputJson))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)
apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status())
assert.EqualValues(t, "Please enter a valid title", apiErr.Message())
assert.EqualValues(t, "invalid_request", apiErr.Error())
}

//Other errors can happen when we try to update the message


func TestUpdateMessage_Error_Updating(t *testing.T) {
services.MessagesService = &serviceMock{}
updateMessageService = func(message *domain.Message) (*domain.Message,
error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("error when updating
message")
}
jsonBody := `{"title": "update title", "body": "update body"}`
r := gin.Default()
id := "1"
req, err := http.NewRequest(http.MethodPut, "/messages/"+id,
bytes.NewBufferString(jsonBody))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.PUT("/messages/:message_id", UpdateMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)

assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status())


assert.EqualValues(t, "error when updating message", apiErr.Message())
assert.EqualValues(t, "server_error", apiErr.Error())
}
///////////////////////////////////////////////////////////////
40 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

// End of "UpdateMessage" test cases


///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "DeleteMessage" test cases
///////////////////////////////////////////////////////////////
func TestDeleteMessage_Success(t *testing.T) {
services.MessagesService = &serviceMock{}
deleteMessageService = func(msg int64) error_utils.MessageErr {
return nil
}
r := gin.Default()
id := "1"
req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.DELETE("/messages/:message_id", DeleteMessage)
r.ServeHTTP(rr, req)

var response = make(map[string]string)


theErr := json.Unmarshal(rr.Body.Bytes(), &response)
if theErr != nil {
t.Errorf("this is the error: %v\n", err)
}
assert.EqualValues(t, http.StatusOK, rr.Code)
assert.EqualValues(t, response["status"], "deleted")
}

//We wont call the service Delete method here, so no need to mock it
func TestDeleteMessage_Invalid_Id(t *testing.T) {

r := gin.Default()
id := "abc"
req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.DELETE("/messages/:message_id", DeleteMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


if err != nil {
t.Errorf("this is the error: %v\n", err)
}
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "message id should be a number", apiErr.Message())
assert.EqualValues(t, "bad_request", apiErr.Error())
41 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

//If for any reason, our update didnt happen(e.g server error, etc), This is an
error from the service, but the controller conditions where met.
//Maybe the message does not exist, or the server timeout
func TestDeleteMessage_Failure(t *testing.T) {
services.MessagesService = &serviceMock{}
deleteMessageService = func(msg int64) error_utils.MessageErr {
return error_utils.NewInternalServerError("error deleting message")
}
r := gin.Default()
id := "1"
req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.DELETE("/messages/:message_id", DeleteMessage)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


if err != nil {
t.Errorf("this is the error: %v\n", err)
}
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status())
assert.EqualValues(t, "error deleting message", apiErr.Message())
assert.EqualValues(t, "server_error", apiErr.Error())
}
///////////////////////////////////////////////////////////////
// End of "DeleteMessage" test cases
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Start of "GetAllMessages" test cases
///////////////////////////////////////////////////////////////
func TestGetAllMessages_Success(t *testing.T) {
services.MessagesService = &serviceMock{}
getAllMessageService = func() ([]domain.Message, error_utils.MessageErr) {
return []domain.Message{
{
Id: 1,
Title: "first title",
Body: "first body",
},
{
Id: 2,
Title: "second title",
Body: "second body",
},
}, nil
}
42 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

r := gin.Default()
req, err := http.NewRequest(http.MethodGet, "/messages", nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.GET("/messages", GetAllMessages)
r.ServeHTTP(rr, req)

var messages []domain.Message


theErr := json.Unmarshal(rr.Body.Bytes(), &messages)
if theErr != nil {
t.Errorf("this is the error: %v\n", err)
}
assert.Nil(t, err)
assert.NotNil(t, messages)
assert.EqualValues(t, messages[0].Id, 1)
assert.EqualValues(t, messages[0].Title, "first title")
assert.EqualValues(t, messages[0].Body, "first body")
assert.EqualValues(t, messages[1].Id, 2)
assert.EqualValues(t, messages[1].Title, "second title")
assert.EqualValues(t, messages[1].Body, "second body")
}

//For any reason we could not get the messages


func TestGetAllMessages_Failure(t *testing.T) {
services.MessagesService = &serviceMock{}
getAllMessageService = func() ([]domain.Message, error_utils.MessageErr) {
return nil, error_utils.NewInternalServerError("error getting messages")
}
r := gin.Default()
req, err := http.NewRequest(http.MethodGet, "/messages", nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.GET("/messages", GetAllMessages)
r.ServeHTTP(rr, req)

apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes())


assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, "error getting messages", apiErr.Message())
assert.EqualValues(t, "server_error", apiErr.Error())
assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status())
}

go test -cover //running test with coverage.

All tests passed!

43 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Step 6: Running the API


We need to perform basic crud(create read, update and delete) action on messages, so we need to define
routing to help us navigate. From the root directory(/efficient-api/), create the app directory

mkdir app

Then create the routes.go file inside the app directory

cd app && routes.go

package app

import "efficient-api/controllers"

func routes() {
router.GET("/messages/:message_id", controllers.GetMessage)
router.GET("/messages", controllers.GetAllMessages)
router.POST("/messages", controllers.CreateMessage)
router.PUT("/messages/:message_id", controllers.UpdateMessage)
router.DELETE("/messages/:message_id", controllers.DeleteMessage)
}

Still, in the app directory, create the app.go file:

touch app.go

package app

import (
"efficient-api/domain"
"fmt"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"log"
"os"
)

var (
router = gin.Default()
)

func init() {

44 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

//loads values from .env into the system


if err := godotenv.Load(); err != nil {
log.Print("sad .env file found")
}
}

func StartApp() {

dbdriver := os.Getenv("DBDRIVER")
username := os.Getenv("USERNAME")
password := os.Getenv("PASSWORD")
host := os.Getenv("HOST")
database := os.Getenv("DATABASE")
port := os.Getenv("PORT")

domain.MessageRepo.Initialize(dbdriver, username, password, port, host,


database)
fmt.Println("DATABASE STARTED")

routes()

router.Run(":8080")
}

You could see that we called the routes function we defined earlier, and we initialized the database
connection.

Now, let start the app.

In the root directory(path: /efficient-api/), create the main.go file:

touch main.go

package main

import (
"efficient-api/app"
"fmt"
)

func main() {
fmt.Println("Welcome to the app")
app.StartApp()
}

45 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

go run main.go

Your application structure should look like this:

46 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

The integration_tests directory will be created in step 7.

Now, you can use curl or postman to create, get, delete or update a message.

This is a sample post request:

47 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Step 7: Integration Tests.


Thus far, all tests above have been unit tests. In this step, we are going to see an integration test that hits a
real testing database. It a good idea that you also use a real database while testing.

In the root directory(path: /efficient-api/), create the integration_tests directory.

mkdir integration_tests

Remember, a real database connection will be involved here. So we will have functions that refresh and seed
the database before any test is run.

Let’s create a file that will have this setup.

Inside the integration_tests directory, create the setup_test.go file:

cd integration_tests && setup_test.go

package integration__tests

48 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

import (
"database/sql"
"efficient-api/domain"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
"log"
"os"
"testing"
"time"
)

const (
queryTruncateMessage = "TRUNCATE TABLE messages;"
queryInsertMessage = "INSERT INTO messages(title, body, created_at) VALUES(?,
?, ?);"
queryGetAllMessages = "SELECT id, title, body, created_at FROM messages;"
)
var (
dbConn *sql.DB
)

func TestMain(m *testing.M) {


var err error
err = godotenv.Load(os.ExpandEnv("./../.env"))
if err != nil {
log.Fatalf("Error getting env %v\n", err)
}
os.Exit(m.Run())
}

func database() {
dbDriver := os.Getenv("DBDRIVER_TEST")
username := os.Getenv("USERNAME_TEST")
password := os.Getenv("PASSWORD_TEST")
host := os.Getenv("HOST_TEST")
database := os.Getenv("DATABASE_TEST")
port := os.Getenv("PORT_TEST")

dbConn = domain.MessageRepo.Initialize(dbDriver, username, password, port,


host, database)
}

func refreshMessagesTable() error {

stmt, err := dbConn.Prepare(queryTruncateMessage)


if err != nil {
panic(err.Error())
}
_, err = stmt.Exec()
if err != nil {
log.Fatalf("Error truncating messages table: %s", err)
}
return nil
}
49 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

func seedOneMessage() (domain.Message, error) {


msg := domain.Message{
Title: "the title",
Body: "the body",
CreatedAt: time.Now(),
}
stmt, err := dbConn.Prepare(queryInsertMessage)
if err != nil {
panic(err.Error())
}
insertResult, createErr := stmt.Exec(msg.Title, msg.Body, msg.CreatedAt)
if createErr != nil {
log.Fatalf("Error creating message: %s", createErr)
}
msgId, err := insertResult.LastInsertId()
if err != nil {
log.Fatalf("Error creating message: %s", createErr)
}
msg.Id = msgId
return msg, nil
}

func seedMessages() ([]domain.Message, error) {


msgs := []domain.Message{
{
Title: "first title",
Body: "first body",
CreatedAt: time.Now(),
},
{
Title: "second title",
Body: "second body",
CreatedAt: time.Now(),
},
}
stmt, err := dbConn.Prepare(queryInsertMessage)
if err != nil {
panic(err.Error())
}
for i, _ := range msgs {
_, createErr := stmt.Exec(msgs[i].Title, msgs[i].Body, msgs[i].CreatedAt)
if createErr != nil {
return nil, createErr
}
}
get_stmt, err := dbConn.Prepare(queryGetAllMessages)
if err != nil {
return nil, err
}
defer stmt.Close()

rows, err := get_stmt.Query()


if err != nil {
50 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

return nil, err


}
defer rows.Close()

results := make([]domain.Message, 0)

for rows.Next() {
var msg domain.Message
if getError := rows.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt);
getError != nil {
return nil, err
}
results = append(results, msg)
}
return results, nil
}

Ensure that you have created a test database and have the database details in your .env file.

You can write integration tests for the domain methods, as they interact with the database, the services
methods by calling the real domain methods, or the controller functions that call the services, which call the
domain methods that interact with the database.

We will only consider Integration tests for the controller functions, as they will call the services which call the
domains that interact with the database.

Create message_controller_integration_test.go file inside the integration_tests directory:

touch message_controller_integration_test.go

package integration__tests

import (
"bytes"
"efficient-api/controllers"
"efficient-api/domain"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"log"
"net/http"
"net/http/httptest"
"strconv"
"testing"
)

func TestCreateMessage(t *testing.T) {

51 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

database()

gin.SetMode(gin.TestMode)

err := refreshMessagesTable()
if err != nil {
log.Fatal(err)
}
samples := []struct {
inputJSON string
statusCode int
title string
body string
errMessage string
}{
{
inputJSON: `{"title":"the title", "body": "the body"}`,
statusCode: 201,
title: "the title",
body: "the body",
errMessage: "",
},
{
inputJSON: `{"title":"the title", "body": "the body"}`,
statusCode: 500,
errMessage: "title already taken",
},
{
inputJSON: `{"title":"", "body": "the body"}`,
statusCode: 422,
errMessage: "Please enter a valid title",
},
{
inputJSON: `{"title":"the title", "body": ""}`,
statusCode: 422,
errMessage: "Please enter a valid body",
},
{
//when an integer is used like a string for title
inputJSON: `{"title": 12345, "body": "the body"}`,
statusCode: 422,
errMessage: "invalid json body",
},
{
//when an integer is used like a string for body
inputJSON: `{"title": "the title", "body": 123453 }`,
statusCode: 422,
errMessage: "invalid json body",
},
}
for _, v := range samples {
r := gin.Default()
r.POST("/messages", controllers.CreateMessage)
req, err := http.NewRequest(http.MethodPost, "/messages",
52 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

bytes.NewBufferString(v.inputJSON))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

responseMap := make(map[string]interface{})
err = json.Unmarshal(rr.Body.Bytes(), &responseMap)
if err != nil {
t.Errorf("Cannot convert to json: %v", err)
}
fmt.Println("this is the response data: ", responseMap)
assert.Equal(t, rr.Code, v.statusCode)
if v.statusCode == 201 {
//casting the interface to map:
assert.Equal(t, responseMap["title"], v.title)
assert.Equal(t, responseMap["body"], v.body)
}
if v.statusCode == 400 || v.statusCode == 422 || v.statusCode == 500 &&
v.errMessage != "" {
assert.Equal(t, responseMap["message"], v.errMessage)
}
}
}

func TestGetMessageByID(t *testing.T) {

database()

gin.SetMode(gin.TestMode)

err := refreshMessagesTable()
if err != nil {
log.Fatal(err)
}
message, err := seedOneMessage()
if err != nil {
t.Errorf("Error while seeding table: %s", err)
}

samples := []struct {
id string
statusCode int
title string
body string
errMessage string
}{
{
id: strconv.Itoa(int(message.Id)),
statusCode: 200,
title: message.Title,
body: message.Body,
errMessage: "",
53 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

},
{
id: "unknwon",
statusCode: 400,
errMessage: "message id should be a number",
},
{
id: strconv.Itoa(12322), //an id that does not exist
statusCode: 404,
errMessage: "no record matching given id",
},
}
for _, v := range samples {
r := gin.Default()
r.GET("/messages/:message_id", controllers.GetMessage)
req, err := http.NewRequest(http.MethodGet, "/messages/"+v.id, nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

responseMap := make(map[string]interface{})
err = json.Unmarshal(rr.Body.Bytes(), &responseMap)
if err != nil {
t.Errorf("Cannot convert to json: %v", err)
}
assert.Equal(t, rr.Code, v.statusCode)

if v.statusCode == 200 {
//casting the interface to map:
assert.Equal(t, responseMap["title"], v.title)
assert.Equal(t, responseMap["body"], v.body)
}
if v.statusCode == 400 || v.statusCode == 422 && v.errMessage != "" {
assert.Equal(t, responseMap["message"], v.errMessage)
}
}
}

func TestUpdateMessage(t *testing.T) {

database()

gin.SetMode(gin.TestMode)

err := refreshMessagesTable()
if err != nil {
log.Fatal(err)
}
messages, err := seedMessages()
if err != nil {
t.Errorf("Error while seeding table: %s", err)
}
54 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

//Get only the first message id


firstId := messages[0].Id

samples := []struct {
id string
inputJSON string
statusCode int
title string
body string
errMessage string
}{
{
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title":"update title", "body": "update body"}`,
statusCode: 200,
title: "update title",
body: "update body",
errMessage: "",
},
{
// "second title" belongs to the second message so, the cannot be used
for the first message
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title":"second title", "body": "update body"}`,
statusCode: 500,
errMessage: "title already taken",
},
{
//Empty title
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title":"", "body": "update body"}`,
statusCode: 422,
errMessage: "Please enter a valid title",
},
{
//Empty body
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title":"the title", "body": ""}`,
statusCode: 422,
errMessage: "Please enter a valid body",
},
{
//when an integer is used like a string for title
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title": 12345, "body": "the body"}`,
statusCode: 422,
errMessage: "invalid json body",
},
{
//when an integer is used like a string for body
id: strconv.Itoa(int(firstId)),
inputJSON: `{"title": "the title", "body": 123453 }`,
statusCode: 422,
55 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

errMessage: "invalid json body",


},
{
id: "unknwon",
statusCode: 400,
errMessage: "message id should be a number",
},
{
id: strconv.Itoa(12322), //an id that does not exist
inputJSON: `{"title":"the title", "body": "the body"}`,
statusCode: 404,
errMessage: "no record matching given id",
},
}
for _, v := range samples {
r := gin.Default()
r.PUT("/messages/:message_id", controllers.UpdateMessage)
req, err := http.NewRequest(http.MethodPut, "/messages/"+v.id,
bytes.NewBufferString(v.inputJSON))
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

responseMap := make(map[string]interface{})
err = json.Unmarshal(rr.Body.Bytes(), &responseMap)
if err != nil {
t.Errorf("Cannot convert to json: %v", err)
}
assert.Equal(t, rr.Code, v.statusCode)
if v.statusCode == 200 {
//casting the interface to map:
assert.Equal(t, responseMap["title"], v.title)
assert.Equal(t, responseMap["body"], v.body)
}
if v.statusCode == 400 || v.statusCode == 422 || v.statusCode == 500 &&
v.errMessage != "" {
assert.Equal(t, responseMap["message"], v.errMessage)
}
}
}

func TestGetAllMessage(t *testing.T) {

database()

gin.SetMode(gin.TestMode)

err := refreshMessagesTable()
if err != nil {
log.Fatal(err)
}
_, err = seedMessages()
56 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

if err != nil {
t.Errorf("Error while seeding table: %s", err)
}
r := gin.Default()
r.GET("/messages", controllers.GetAllMessages)

req, err := http.NewRequest(http.MethodGet, "/messages", nil)


if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

var msgs []domain.Message

err = json.Unmarshal(rr.Body.Bytes(), &msgs)


if err != nil {
log.Fatalf("Cannot convert to json: %v\n", err)
}
assert.Equal(t, rr.Code, http.StatusOK)
assert.Equal(t, len(msgs), 2)
}

func TestDeleteMessage(t *testing.T) {

database()

gin.SetMode(gin.TestMode)

err := refreshMessagesTable()
if err != nil {
log.Fatal(err)
}
message, err := seedOneMessage()
if err != nil {
t.Errorf("Error while seeding table: %s", err)
}
samples := []struct {
id string
statusCode int
status string
errMessage string
}{
{
id: strconv.Itoa(int(message.Id)),
statusCode: 200,
status: "deleted",
errMessage: "",
},
{
id: "unknwon",
statusCode: 400,
errMessage: "message id should be a number",
},
57 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

{
id: strconv.Itoa(12322), //an id that does not exist
statusCode: 404,
errMessage: "no record matching given id",
},
}
for _, v := range samples {
r := gin.Default()
r.DELETE("/messages/:message_id", controllers.DeleteMessage)
req, err := http.NewRequest(http.MethodDelete, "/messages/"+v.id, nil)
if err != nil {
t.Errorf("this is the error: %v\n", err)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)

responseMap := make(map[string]interface{})
err = json.Unmarshal(rr.Body.Bytes(), &responseMap)
if err != nil {
t.Errorf("Cannot convert to json: %v", err)
}
assert.Equal(t, rr.Code, v.statusCode)

if v.statusCode == 200 {
//casting the interface to map:
assert.Equal(t, responseMap["status"], v.status)
}
if v.statusCode == 400 || v.statusCode == 422 && v.errMessage != "" {
assert.Equal(t, responseMap["message"], v.errMessage)
}
}
}

We ensured that we used Table Tests, so we can test all edge cases with one test function.

No test function depend on the other, you can see that before any test is executed, the database function
and the refreshMessagesTable are always called.

While in the integration_tests directory, run the test suite:

go test

All tests passed. How Sweet!

Note that the controller integration tests called the real service and domain methods. You can write the
integration tests for the service and the domain methods if you wish.

Get the code on github. You can drop a star.


Conclusion
58 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023

Both Unit and Integration tests are important in an application. They give the developer more confidence,
they also help identify bugs. Writing tests can save you from a lot of trouble. I understand that it is tedious to
write sometimes, but it is worth it.

Thank you.

You can follow me on twitter for notifications on new articles.

Happy Testing.

59 / 59

You might also like