Understanding Unit and Integration Testing in Golang
Understanding Unit and Integration Testing in Golang
md 1/17/2023
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.
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.
mkdir efficient-api
cd 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
efficient-api
├── .env
└── go.mod
2 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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
mkdir utils
i. error_utils:
This package has methods that handle different kinds of errors we may encounter in the application.
3 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
package error_utils
import (
"encoding/json"
"net/http"
)
4 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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:
package error_formats
import (
"efficient-api/utils/error_utils"
"fmt"
"github.com/go-sql-driver/mysql"
"strings"
)
5 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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.
mkdir domain
i. DTO.
package domain
import (
"efficient-api/utils/error_utils"
"strings"
"time"
)
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
touch message_dao.go
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;"
)
return mr.db
}
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
}
defer stmt.Close()
9 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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.
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.
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"
)
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)
}
})
}
}
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)
}
})
}
}
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)
}
})
}
}
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
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
}
})
}
}
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.
go test
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.
mkdir services
package services
import (
"efficient-api/domain"
"efficient-api/utils/error_utils"
"time"
)
var (
MessagesService messageServiceInterface = &messagesService{}
)
20 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
if err != nil {
return nil, err
}
return messages, 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.
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)
)
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)
}
///////////////////////////////////////////////////////////////
// Start of "CreateMessage" test cases
///////////////////////////////////////////////////////////////
//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
//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{}
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())
}
///////////////////////////////////////////////////////////////
// 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")
}
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.
go test
go test -cover
Step 5: Controllers
In the root directory(path: /efficient-api/), create the controllers directory:
mkdir controllers
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
}
c.JSON(err.Status(), err)
return
}
c.JSON(http.StatusCreated, msg)
}
From the above, we used the gin-gonic framework. Gin is a cool package for request handling, response
handling, and routing.
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).
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)
)
///////////////////////////////////////////////////////////////
// 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)
//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)
r := gin.Default()
req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil)
rr := httptest.NewRecorder()
r.GET("/messages/:message_id", GetMessage)
r.ServeHTTP(rr, req)
///////////////////////////////////////////////////////////////
// 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)
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
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)
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
//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)
38 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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())
}
///////////////////////////////////////////////////////////////
// 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)
//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)
//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)
///////////////////////////////////////////////////////////////
// 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)
43 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
mkdir app
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)
}
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
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")
routes()
router.Run(":8080")
}
You could see that we called the routes function we defined earlier, and we initialized the database
connection.
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
46 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
Now, you can use curl or postman to create, get, delete or update a message.
47 / 59
Understanding Unit and Integration Testing in Golang.md 1/17/2023
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.
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 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")
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.
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"
)
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)
}
}
}
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)
}
}
}
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
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
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)
}
}
}
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)
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.
go test
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.
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.
Happy Testing.
59 / 59