Skip to main content

Golang Parallel Test and Subtest Execution

 Introduction

In this section I will discuss the basics of unit testing, subtesting and parallel test execution in Go. Also I'm gonna demonstrate a unit test package  stretchr / testify which makes testing the code extremely easy. This post will be full of code snippets and their descriptions, so if you face any difficulties leave a comment at the end of the blog and I will get back to you as soon as possible.

Unit Testing

It is a methodology and language independent concept where we perform testing on all the code components like functions, methods, variables etc. The source code is tested for an objective purpose to prove that each unit of code is performing as expected. For more details you can visit Unit test basics.

Go has it's own built-in testing package which needs to be imported while writing unit tests. Lets get started with some code. First of all I'm gonna create a directory `jsonserializer` and create a file `serializer.go` withing that. Add the following code to `serializer.go` file.

package serializer

import (
"encoding/json"
"io/ioutil"
"log"
"time"
)

var fileNamefilename = "tmp/user.json"

type User struct {
Id int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}

func WriteStructToJSONFile(listofusers []*User, fileName string) error {
data, err := json.Marshal(listofusers)
if err != nil {
log.Println("couldn't marshal to json")
return err
}

if err := ioutil.WriteFile(fileName, data, 0644); err != nil {
log.Println("couldn't write to a file")
return nil
}
return nil
}

func FromJSONFileToStruct(fileName string) ([]*User, error) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
log.Println("couldn't read the json file")
return nil, err
}

var users []*User
if err := json.Unmarshal(data, &users); err != nil {
return nil, err
}

return users, nil
}

func CreateUsers() ([]*User, error) {
users := []*User{
&User{
Id: 1,
FirstName: "Dave",
LastName: "Augustus",
Email: "dave@mail.com",
CreatedAt: time.Now(),
},
&User{
Id: 1,
FirstName: "Irak",
LastName: "Rigia",
Email: "ir@mail.com",
CreatedAt: time.Now(),
},
&User{
Id: 3,
FirstName: "Imaginery",
LastName: "User",
Email: "iu@gmail.com",
CreatedAt: time.Now(),
},
}

return users, nil
}


Here in the above code I have written three functions to convert data from structure to JSON file and from JSON file to structure. Here the CreateUser function returns a slice of `User` struct which has three items.

Okay we are good till here, now we need to move our terminal's working directory to `jsonserializer` folder and create a `go mod file` using the following command.

jsonserializer$ go mod init github.com/davetweetlive/jsonserializer

Here you can pick your Github username instead of davetweetlive and also create a `tmp` folder in `jsonserializer` as it's needed to hold `json` file.

Now create a file `serializer_test.go` in same folder `jsonserializer` and paste the below code in the file. 

package serializer_test

import (
"testing"

jsonserializer "github.com/davetweetlive/jsonserializer"
)

var fileName = "tmp/user.json"

func TestCreateUsers(t *testing.T) {
users, err := jsonserializer.CreateUsers()
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}

if len(users) != 3 {
t.Errorf("users length should be 3, but got %v", len(users))
}

if users[1].FirstName != "Irak" {
t.Errorf("the firstname should be Irak but got %v", users[1].FirstName)
}
}

func TestWriteStructToJSONFile(t *testing.T) {
users, err := jsonserializer.CreateUsers()
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}

err = jsonserializer.WriteStructToJSONFile(users, fileName)
if err != nil {
t.Errorf("Couldn't write to JSON file")
}
}

func TestFromJSONFileToStruct(t *testing.T) {
users, err := jsonserializer.FromJSONFileToStruct(fileName)
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}
if len(users) != 3 {
t.Errorf("users length should be 3, but got %v", len(users))
}

if users[1].FirstName != "Irak" {
t.Errorf("the firstname should be Irak but got %v", users[1].FirstName)
}
}


Once done then, run jsonserializer$ go test -v command from you terminal and you should get the following output.

=== RUN   TestCreateUsers
--- PASS: TestCreateUsers (0.00s)
=== RUN   TestWriteStructToJSONFile
--- PASS: TestWriteStructToJSONFile (0.00s)
=== RUN   TestFromJSONFileToStruct
--- PASS: TestFromJSONFileToStruct (0.00s)
PASS
ok      github.com/davetweetlive/jsonserializer    0.001s


Here above we are testing all the functions individually and if you see the output, it tests the functions from the beginning basically means that the functions defined at the top will be tested first. To test this you can re-range the functions in different orders and see the output.

Or you can add `time.Sleep(time.Second)` in all the test function as the first line for example.

func TestWriteStructToJSONFile(t *testing.T) {
time.Sleep(time.Second)
users, err := jsonserializer.CreateUsers()
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}

err = jsonserializer.WriteStructToJSONFile(users, fileName)
if err != nil {
t.Errorf("Couldn't write to JSON file")
}
}

Here above I have shown example of one function but you need to put sleep function in all three. Now try running the test command.


jsonserializer$ go test -v 

And you will see that all the functions are executing in the same sequence but at an interval of one second. Although we didn't need the sleep function, I put it in the code to show you the execution sequence at one second intervals. So think if we have thousands of functions executed sequentially wouldn't it be time consuming for CPU? Yes it will!

To solve this problem we have Parallel test.

Parallel Test Execution

This is the simplest, in the above three test cases every time you run `go test -v` command the test is first started from the top because this is how our code executes, but it is not a best practice when we have thousands of test cases, in that case testing may take a little longer. So to overcome this what we need to do is put t.Parallel() in each test functions at the beginning and try running `go test -v` command on the terminal. You will find all the tests are executing in parallel. To confirm this you can put time.Sleep function again in the code. Checkout the below example:

func TestFromJSONFileToStruct(t *testing.T) {
t.Parallel()
time.Sleep(time.Second)
users, err := jsonserializer.FromJSONFileToStruct(fileName)
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}
if len(users) != 3 {
t.Errorf("users length should be 3, but got %v", len(users))
}

if users[1].FirstName != "Irak" {
t.Errorf("the firstname should be Irak but got %v", users[1].FirstName)
}
}

Make sure you add `t.Parallel()` in all three function before running `go test -v`

If you wanna test code coverage percentage then run `go test -v --cover`

Now you may or may not remove the time.Sleep function...


Subtest Execution

Consider if you don't want to execute all three test functions and choose to run one of the test you can run the following command.

jsonserializer$ go test -run FromJSONFileToStruct

Here we are using -run parameters after which the function output can be seen below.

PASS
ok      github.com/davetweetlive/jsonserializer    1.003s


Run method on the T and B types that allows for the creation of subtests and sub-benchmarks. Subtest enables better handling of faliure and controll of parallalism. In below code everytime we do `t.Run()` which means we are creating a new test. We pass name of the test as the first parameter and then a function which receives the pointer to `T`.

func TestCreateUsersSubTest(t *testing.T) {
users, err := jsonserializer.CreateUsers()
if err != nil {
t.Errorf("error shoube be nil, but got %v", err)
}

batch := []struct {
batch int
users []*jsonserializer.User
}{
{batch: 1, users: users},
{batch: 2, users: []*jsonserializer.User{
{
Id: 1,
FirstName: "ABC",
LastName: "Some Last Name",
Email: "abc@mail.com",
CreatedAt: time.Now(),
},
{
Id: 3,
FirstName: "PQR",
LastName: "Alphabet",
Email: "pa@gmail.com",
CreatedAt: time.Now(),
},
},
},
{
batch: 3,
users: []*jsonserializer.User{
{
Id: 3,
FirstName: "XYZ",
LastName: "Alphabet",
},
},
},
}

if batch == nil {
t.Errorf("Errro %v", errors.New("batch shouldn't be empty"))
}

for i, tc := range batch {
t.Run(strconv.Itoa(tc.batch), func(t *testing.T) {
t.Parallel()
if tc.batch < 1 || tc.batch > 3 {
t.Errorf("Batch can't be more then three or less than equal to zeor, got %v", tc.batch)
if tc.users[i].FirstName == "" {
t.Errorf("firstname can't be empty")
}
if tc.users[i].LastName == "" {
t.Errorf("lastname can't be empty")
}
if tc.users[i].Email == "" {
t.Errorf("email can't be empty")
}
}

if len(tc.users) == 0 {
t.Error("user's list shouldn't be zero, got ", len(tc.users))
}

})
}
}

 Testing using  stretchr / testify 

This package has made testing a lot easier. Here I'm gonna remove all the `t.Errorf `method and use the testify package instead I will be removing all the `if` condititions too. Please checkout the code below.

before copying the code make sure you import the testify package using the following command.

jsonserializer$ go get github.com/stretchr/testify

package serializer_test

import (
"strconv"
"testing"
"time"

jsonserializer "github.com/davetweetlive/jsonserializer"
"github.com/stretchr/testify/require"
)

var fileName = "tmp/user.json"

func TestCreateUsers(t *testing.T) {
t.Parallel()
users, err := jsonserializer.CreateUsers()
require.NoError(t, err)
require.Equal(t, len(users), 3)
require.Equal(t, users[1].FirstName, "Irak")
}

func TestWriteStructToJSONFile(t *testing.T) {
t.Parallel()
users, err := jsonserializer.CreateUsers()
require.NoError(t, err)

err = jsonserializer.WriteStructToJSONFile(users, fileName)
require.NoError(t, err)
}

func TestFromJSONFileToStruct(t *testing.T) {
t.Parallel()
users, err := jsonserializer.FromJSONFileToStruct(fileName)
require.NoError(t, err)
require.Equal(t, len(users), 3)
require.Equal(t, users[2].FirstName, "Imaginery")
}

func TestCreateUsersSubTest(t *testing.T) {
users, err := jsonserializer.CreateUsers()
require.NoError(t, err)
batch := []struct {
batch int
users []*jsonserializer.User
}{
{batch: 1, users: users},
{batch: 2, users: []*jsonserializer.User{
{
Id: 1,
FirstName: "ABC",
LastName: "Some Last Name",
Email: "abc@mail.com",
CreatedAt: time.Now(),
},
{
Id: 3,
FirstName: "PQR",
LastName: "Alphabet",
Email: "pa@gmail.com",
CreatedAt: time.Now(),
},
},
},
{
batch: 3,
users: []*jsonserializer.User{
{
Id: 3,
FirstName: "XYZ",
LastName: "Alphabet",
},
},
},
}

require.NotEqual(t, batch, nil)

for i, tc := range batch {
t.Run(strconv.Itoa(tc.batch), func(t *testing.T) {
t.Parallel()
if tc.batch < 1 || tc.batch > 3 {

require.NotEqual(t, tc.users[i].FirstName, "")

require.NotEqual(t, tc.users[i].LastName, "")

require.NotEqual(t, tc.users[i].Email, "")
}
require.NotEqual(t, len(tc.users), 0)
})
}
}


That's all we have in this section, you can find the code on https://github.com/Saffron-Coders/go-testing

If you have questions on Golang, drop your comments below and I thank you eveyone for reading.

Comments

Popular posts from this blog

Reverse engineering - Get root shell access to D-Link router using UART(Get WiFi password)

Introduction Hello there. In this article I will be showing you a very commonly used reverse engineering and hardware hacking technique to gain root access to the shell of a WiFi router and retrieve the WLAN SSID and password. It also works for many other kinds of embedded devices too but here I will be showing you my experience with a WiFi router D-Link DSL-2730U. This is my first blog ever so it is a brand new experience for me. Although I will do my best to make this content a very valuable and interesting one. NOTE: If you find any incomplete, missing or even misleading information here, then please put it in the comments. I would love to learn even more. Prerequisites It is best if you have a fair amount of experience in the following: Linux fundamentals and sysadmin skills. Fair amount of hands-on experience with Arduino or Raspberry Pi projects. Building custom Linux systems for embedded devices(Buildroot, Yocto, Busybox, etc.). Protocols like - UART, SPI, I2C, etc. You do not n

Little on Linux

I used to be an average Windows operating system user, who was just using a system to browse the internet and play computer games, then my friends  Irak Rigia  introduced me to Linux. My early days with the newly introduced OS were not so good, I used the Ubuntu GUI to perform my basics tasks such as moving files and deleting files. But gradually I started learning terminal basic commands and things changed. Now I have four inter-connected Linux machines  at home which I use to learn Linux and networking. Now I have found linux in a straightforward form, and understood the principle behind an operating system. The intention of this article is not to show Windows as a bad OS, they are great and also amazing in terms of user interface, but they don't reveal what goes behind the scene. But again it’s personal preference. Ok, that was about my journey with Linux, now let me tell you that how you can be a power user? and what can be accomplished using Linux? Linux