Getting started with Automated Testing in Go

Simant Thapa Magar
readytowork, Inc.
Published in
12 min readMar 23, 2024

It is always encouraged to write tests for the application as it helps to verify that the application is fulfilling its intended responsibilities correctly. As the modern approach for application development is modular programming, being able to verify each function works properly makes them a source of truth and develops a sense of confidence. Moreover, test codes can be used again and again so with the introduction of new feature you won’t need to worry about testing everything from the start manually and covering humongous scenarios. Also, test codes in large codebases can serve as documents to help understand the purpose of function.

Automated testing is possible without the installation of any additional package with go as you have access to go test runner right out of the box and testing standard package for performing necessary functionalities.

In this article, we will be getting started with writing automated test in go using the standard packages as well as other third-party packages. So first we will start writing test code without any external package and for that we will start by running the init command

go mod init github.com/Simant-Thapa-Magar/go-test

We will create a simple application where we will have a type Order which contains few attributes including another type Item . This relates to a simple real-world order containing information on the price and quantity of items. The type Order has a function GetTotal which calculates the total amount of order by looping through the slice of Item and determining sum of each item’s cost and returning the value of type float64 but truncated to 2 digit as it may contain garbage value for longer format. So our order.go file will look as follows

package order

import (
“math”
)

type Item struct {
Id string
Quantity int
UnitPrice float64
}

type Order struct {
Id string
Items []Item
}

func (o Order) GetTotal() float64 {
var total float64
for _, item := range o.Items {
total += item.UnitPrice * float64(item.Quantity)
}
return math.Floor(total*100) / 100
}

Using Standard testing Package

Now that we have a very simple application ready, lets start writing test codes to verify that GetTotal returns correct value. The test file in go should be in same directory ending with _test.go suffix. So we will create a new file order_test.go. Just like naming convention for the file, there’s a standard naming convention and arguments defined for a test function. The test function should start with Test prefix and takes an argument reference to the standard package testing which we don’t need to worry about. So a testing is simply verifying whether something is behaving correctly or not under certain scenarios and comparing the expected output with received output. To test GetTotal the function we will create a variable of type Order with some Item data whose total shall be calculated manually and compare it with the total received by running the function. If the values are same then all’s good, otherwise, we will use Errorf the function available with testing the package to log that test failed. So the order_test.go file will look as follow

package order

import "testing"

func TestGetTotal(t *testing.T) {
order := Order{
Id: "1",
Items: []Item{
{
Id: "1",
Quantity: 2,
UnitPrice: 2.5,
},
{
Id: "2",
Quantity: 5,
UnitPrice: 4.99,
},
},
}

total := order.GetTotal()
shouldBeTotal := 29.95

if total != shouldBeTotal {
t.Errorf("Got %f, wanted %f", total, shouldBeTotal)
}
}

Its a very simple function similar to any other normal function that we would write in go. To run this test we need to run command as below which simply tells to run all the test files along with test info.

go test -v -cover ./…

After running this command we get output as below saying pass which means all of the test passed successfully. Similarly we can test other scenarios with same approach and using comparators such as check if error is returned from a function on invalid arguments, check greater or less than and so on. In case the test fails we see the log.

Using Testify Package

Now let's use an external package testify to test same case as above and also see some additional use cases. First of all, we will get the package using the following command.

go get github.com/stretchr/testify

We will test the same function that returns total of an order using testify under same file. However, we will test for multiple orders this time by creating a slice of Order and looping over them. This approach can be useful when we need to test multiple scenario. Besides slice of Order we will create another slice of float64 containing calculated total for each order respectively. The main advantage of using testify package over just standard testing package is that testify provides assert package which contains a set of useful testing tools. In our previous example we used != operator to check if our test failed but with testify we get NotEqual assertion function to serve same purpose. Similarly we have access to other useful assertion functions like Equal , NotNil, Contains, Panics and many more to serve out testing needs. Each of these asserts accept first argument that references to testing.T making our test function’s skeleton same as previous example. So only difference is on implementation of test condition. Along with testing argument, the assert function can accept expected value, actual value and error message depending upon the nature of function. Hence with these considerations our test function using testify will look as follow.

package order

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestGetTotalUsingTestify(t *testing.T) {
orders := []Order{
{
Id: "1",
Items: []Item{
{
Id: "1",
Quantity: 10,
UnitPrice: 2.49,
},
{
Id: "2",
Quantity: 5,
UnitPrice: 1.99,
},
},
},
{
Id: "2",
Items: []Item{
{
Id: "3",
Quantity: 1,
UnitPrice: 49.95,
},
{
Id: "4",
Quantity: 7,
UnitPrice: 5,
},
},
},
}

shouldBeTotal := []float64{34.85, 84.95}

for index, order := range orders {
total := order.GetTotal()
assert.Equal(t, shouldBeTotal[index], total, "Total should be equal")
}
}

Next running our test using previous go test runner we can see the tests pass and altering the expected or actual values we can create scenario where test fails.

Mock using Testify

The example shown till now doesn’t involve dependency with any other function as GetTotal calculates the total of an order on its own. However, it always won’t be the case. We can come across situations where we need to unit test a function that depends on other function or services. For example a function in controller that expects some interaction with service and repository or function that uses math/rand package to generate a random number or time.Now() function that shows current time. The common thing between these scenarios is that they return dynamic value and isn’t fully in our control. Moreover, we don’t want to add the burden of testing these dependent functions to our unit testing because that would make it integration testing. As a solution testify offers mock to verify the calls are happening as they should and make the function behave in a controlled manner.

In order to illustrate the example of mock lets continue from earlier example and create a new function for type Order say GetALuckyNumber that accepts an interface which contains function randomInt that generates a random number. As the name suggests the function will return a random integer. The function randomInt will behave similar to function Intn from package math/rand. Now the order.go file will look as below

package order

import (
“math”
)

type RandomGenerator interface {
randomInt(max int) int
}

type Item struct {
Id string
Quantity int
UnitPrice float64
}

type Order struct {
Id string
Items []Item
}

func (o Order) GetTotal() float64 {
var total float64
for _, item := range o.Items {
total += item.UnitPrice * float64(item.Quantity)
}
return math.Floor(total*100) / 100
}

func (o Order) GetALuckyNumber(r RandomGenerator) int {
return r.randomInt(10) * 10
}

Now moving towards the test, we will need to do following

  • create a type that contains reference to mock package from testify
  • implement the dependent function i.e. randomInt within new type
  • match the mock call for randomInt function and return a known value
  • test the scenario

The first step is straight forward and looks as follow

import “github.com/stretchr/testify/mock”

type mockOrder struct {
mock.Mock
}

Next we need to implement the dependent function which in our case is randomInt that will return a value. Inside the function we call the Called function from mock package as first step which indicates the method has been called and returns an array of argument. The received value depends on the next step inside test function that we will talk about shortly but for now lets just understand that argument contains an integer at the starting point since the function should return an integer at the end. So we will get the first element from the argument as int and return this value which looks a s follow.

func (mo *mockOrder) randomInt(max int) int {
args := mo.Called(max)
result := args.Int(0)
return result
}

Now we can move in to our test function for mock and implement it. Inside the function first we initialize a new variable of type mockOrder. Next we need start the required method to be called and return a known value so that we can test further using On method passing function name as first argument and other necessary argument. After that we will simply continue with our assert to test the function.


func TestMockUsingTestify(t *testing.T) {
mockOrder := new(mockOrder)
mockOrder.On("randomInt", 10).Return(1)

order := Order{
Id: "1",
Items: []Item{
{
Id: "1",
Quantity: 2,
UnitPrice: 2.5,
},
{
Id: "2",
Quantity: 5,
UnitPrice: 4.99,
},
},
}

luckyNumber := order.GetALuckyNumber(mockOrder)
assert.Equal(t, 1, luckyNumber, "Should get lucky number 10")
}

Again running the test command we can check if our test gets pass for mock as well.

Mock SQL behavior with go-sqlmock

Now let’s try another package named go-sqlmock whose purpose is to simulate the behavior of a sql driver for test without connecting to an actual database. For this we will create a different folder named book where our main function will connect to an actual database and perform few sql queries. I will not go in to much detail of this as we are focusing on test. Just for briefing we will be using gorm as ORM library for go. A table books is created in an actual database with columns referenced in Book struct as shown below. There are primarily two main functions that perform insert & select query which we will test. Here’s the reference code of the main application.

package main

import (
“fmt”
“log”
“time”
“gorm.io/driver/mysql”
“gorm.io/gorm”
)

type Book struct {
Id int64 `gorm:”column:Id”`
Name string `gorm:”column:Name”`
Author string `gorm:”column:Author”`
PublishedDate time.Time `gorm:”column:PublishedDate”`
}

type Service struct {
database *gorm.DB
}

func initializeDatabase() (*gorm.DB, error) {
dsn := “root:@tcp(127.0.0.1:3306)/gotest?charset=utf8mb4&parseTime=True&loc=Local”
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
return db, err
}

func main() {
db, err := initializeDatabase()
if err != nil {
log.Fatal(err)
}

service := &Service{
database: db,
}

service.TruncateTable()

books := GetSomeBooks()

for _, book := range books {
e := service.AddBook(book)
if e != nil {
log.Fatal(e)
}
}

booksFor1904, err := service.GetBookByYear(1904)

if err != nil {
log.Fatal(err)
}

if len(booksFor1904) > 0 {
fmt.Println(“Books published in 1904 are:”)
for i, book := range booksFor1904 {
fmt.Printf(“%d %s by %s\n”, i+1, book.Name, book.Author)
}
}
}

// executes insert query
func (s *Service) AddBook(book Book) error {
err := s.database.Create(&book).Error
return err
}

// executes select query
func (s *Service) GetBookByYear(year int) ([]Book, error) {
var books []Book
err := s.database.Model(&Book{}).Where(“YEAR(PublishedDate) = ?”, year).Find(&books).Error
return books, err
}

func GetSomeBooks() []Book {
var books []Book

publishDate, _ := time.Parse(“2006–01–02”, “1913–01–01”)

books = append(books, Book{
Name: “In Search of Lost Time”,
Author: “Marcel Proust”,
PublishedDate: publishDate,
})

publishDate2, _ := time.Parse(“2006–01–02”, “1904–01–01”)

books = append(books, Book{
Name: “Ulysses”,
Author: “James Joyce”,
PublishedDate: publishDate2,
})

publishDate3, _ := time.Parse(“2006–01–02”, “1599–01–01”)
books = append(books, Book{
Name: “Hamlet”,
Author: “William Shakespeare”,
PublishedDate: publishDate3,
})

return books
}

func (s *Service) TruncateTable() error {
return s.database.Where(“1=1”).Delete(&Book{}).Error
}

If we run this file then we will see that there’s no error and database queries have been executed successfully. But our objective is here is to mock the SQL behavior instead of actually running the queries, so for that lets move towards test. As mentioned earlier the test file should end with _test suffix and be on same directory. So we will create a new file book_test.go

The first thing that we need to do is install the go-sqlmock package so lets run the command for it.

go get gopkg.in/DATA-DOG/go-sqlmock.v1

Using this package and gorm we need to mock a database connection to test our code. So we will create a function that initializes the database connection and returns the required object. The function will accept an argument reference to sql.DB and returns reference to Service after which we can call the functions that execute some SQL query. The sql.DB object can be received by calling sqlmock.New() function from sqlmock package which also returns two other parameters. We will use this function for every test functions that we will create to get the object with database instance.

func getDBObj(mockDB *sql.DB) *Service {
db, err := gorm.Open(mysql.New(mysql.Config{
Conn: mockDB,
SkipInitializeWithVersion: true,
}), &gorm.Config{})

if err != nil {
log.Fatal(err)
}

service := Service{
database: db,
}
return &service
}

Now lets start the test for insert query. Like just mentioned earlier we will get mock database from sqlmock and use it to get the database instance. Next we create a variable of type Book to be created. Now the mocking begins where we first specify that transaction is expected to begin and then we define the behavior for insert query. We expect the execution of insert query with few arguments and expect column id of new data and number of rows effect to be returned as result. Then we expect transaction to be completed. This brings end to defining behavior for insert operation. Now we call the function that triggers this sql behavior which is AddBook function in our case and if no error is returned then our test has passed.

func TestAddBook(t *testing.T) {
mockDB, mock, err := sqlmock.New()

if err != nil {
log.Fatal(err)
}

service := getDBObj(mockDB)

bookSample := Book{
Id: 1,
Name: "Sample Book",
Author: "Sample Author",
PublishedDate: time.Now(),
}

mock.ExpectBegin()
mock.ExpectExec("INSERT").WithArgs(bookSample.Id, bookSample.Name, bookSample.Author, bookSample.PublishedDate).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()

addErr := service.AddBook(bookSample)

if addErr != nil {
t.Errorf("Got error while inserting book: %s", addErr)
}
}

Moving towards the next function, we have TestGetBookByYear which execute a select query. The initial steps of getting a database instance and defining a variable of type Book are same as the earlier function. As the select statement can return multiple rows, we will initialize a row variable that stores table rows. This is done using NewRows a function from sqlmock package where we pass the slice of string as column headers and then call AddRow the function to add rows which will be data from Book type variable. Next is defining the behavior where we expect a select query which can receive arguments and return rows initialized earlier.Now we simply call the function that executes the select statement and now we can test the new function as well.

func TestGetBookByYear(t *testing.T) {
mockDB, mock, err := sqlmock.New()

if err != nil {
log.Fatal(err)
}

service := getDBObj(mockDB)

bookSample := Book{
Id: 1,
Name: “Sample Book”,
Author: “Sample Author”,
PublishedDate: time.Now(),
}

rows := sqlmock.NewRows([]string{“Name”, “Author”, “PublishedDate”}).AddRow(bookSample.Name, bookSample.Author, bookSample.PublishedDate)

mock.ExpectQuery(“SELECT(.*)”).WillReturnRows(rows)

service.GetBookByYear(2023)
}

Continuing with tests for select statement lets test behavior where empty rows are received. It is entirely the same to the above function with only difference being AddRow function is not chained with NewRows the function which looks as below

func TestGetBookByYear2(t *testing.T) {
mockDB, mock, err := sqlmock.New()

if err != nil {
log.Fatal(err)
}

service := getDBObj(mockDB)

emptyRow := sqlmock.NewRows([]string{“Name”, “Author”, “PublishedDate”})

mock.ExpectQuery(“SELECT(.*)”).WithArgs(2020).WillReturnRows(emptyRow)

service.GetBookByYear(2020)
}

Now if we run the test command inside the new folder we can see that all tests have passed successfully.

This is all for getting started with automated testing in go with an overview on some packages. We can explore more on this package to create a complete unit test for an application built in Go.

--

--