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
Post a Comment