Applying unit testing to improve code quality in Go
What is Unit Testing
Unit testing is a process in software engineering which is done by developers to test functions or procedures they’ve made and to make sure if they run properly and return expected output. Unit testing is a level of software testing where individual units/components of a software are tested. It is also known as Component Testing.
The purpose of Unit Testing/Component Testing is to validate that every unit of the software performs as designed. In Ralali, unit testing takes an important part for us (developer) because it makes sure the created business logic runs properly.
Benefits of unit testing
- Unit testing helps maintain the quality of your code. If good unit tests are written and if they’re run Every time any code is changed and pushed into the pipeline, you will be able to catch any defects introduced due to the change you made in your code.
- Finding issues at an early stage. Since unit testing is done by developers who test individual code before integration tests, issues can be resolved without impacting the other pieces of the code. This includes both bugs in the programmer’s implementation and flaws or missing parts of the specification for the unit.
- Debugging is easier. It helps you simplify the debugging process. Only the latest changes made in the code need to be debugged if a test case fails while doing unit testing.
Hands on
Let’s try to make a simple unit testing to see how unit tests work. I’m using Go language but please keep in mind if unit tests can be done with any programming language. Go already has a built-in testing package, provided by Go’s standard library inside testing package. Package testing provides support for unit testing of Go files. It is intended to be utilized in concert with the go test command, which automates execution of any function of the shape.
Prerequisite
I assume you already have Go installed in your local machine, if not you can find official installation manual here.
Let’s prepare our workspace
$ mkdir grade_calculator (inside $GOPATH/src/)
$ cd grade_calculator
$ mkdir grade && touch main.go && touch grade/grade.go
The structure inside $GOPATH/src/grade_calculator will looks like this
In this example we will create a CalculateGrade
function that is used to calculate student grade based on their score.
grade.go
package grade
func CalculateGrade(score int) string {
if score <= 100 && score > 80 {
return "A"
}
if score <= 80 && score > 65 {
return "B"
}
if score <= 65 && score > 50 {
return "C"
}
if score <= 50 && score > 40 {
return "D"
}
return "F"
}
then we call CalculateGrade function in our main.go
package main
import (
"fmt"
"grade_calculator/grade"
)
func main() {
fmt.Println("your grade:", grade.CalculateGrade(78))
}
Run go run main.go in your terminal and the result will look like this
Our function runs as expected, we pass 78 as input param and it returns B as our grade. Next we want to make the test for our function. To test our CalculateGrade
function, we need to make the test file. Several things to note here to make a unit test in Go.
- Testing function must be created in a separate go file with _test.go postfix. Example if you have a file name called parser.go then the test file name must be parser_test.go
- Test function naming convention in Go must start with Test and then function name to test. i.e. (TestFunctionName or TestFunctionName_Success) function name must start with capital.
*testing.T
from package testing must be passed as one and only Test function parameter.
So now let’s create grade_test.go file inside grade directory
grade_test.go
package grade
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCalculateGrade(t *testing.T) {
input := 94
expected := "A"
grade := CalculateGrade(input)
assert.Equal(t, expected, grade)
}
I’m using github.com/stretchr/testify
package because it provides some helpful methods that allow you to easily write better test code in Go. In the above code I’m using assert.Equal()
function that does the comparison between expected and actual output. It checks whether the values given to it are equal or not. It’s one of the handy functions provided by the stretchr/testify
inside assert package.
You can install it with
go get github.com/stretchr/testify
Run the test
go test -v ./grade
The result for our test run as expected with no error. Now let’s try to change the expected result from B into A, then run the test again to check the error message.
One good thing from the assert package is it prints friendly, easy to read failure descriptions like we see above.
Test Coverage
Go has built in support to read test coverage, test coverage is a term that describes how much of a package’s code is exercised by running the package’s tests. We say that the test coverage is 50% if executing the test suite causes 50% of the package’s source statements to be run.
The program that gives test coverage in Go 1.2 is that the latest to take advantage of the tooling support within the Go ecosystem.
Instrument the binary is common to compute test coverage. As example, the GNU gcov program sets breakpoints at branches executed by the binary. As each branch executes, the breakpoint is cleared and therefore the target statements of the branch are marked as ‘covered’.
Test coverage is the percentage of your code covered by the test case. Currently our test function only has 22,2% test coverage, it means there are other cases/scenarios that have not been tested yet.
Go has a pre-build feature to visualize which statement is not covered yet. To check other case that not covered in our test, run
go test ./grade -coverprofile=cover.out && go tool cover -html=cover.out
As we can see right now, we only covered 1 case which input between 80–100 and it returned A. And there are other cases that are not covered yet.
So, let’s complete our test with other scenario to increase it coverage
package grade
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCalculateGrade(t *testing.T) {
scenarios := []struct {
Input int
Result string
}{
{
Input: 94,
Result: "A",
},
{
Input: 73,
Result: "B",
}, {
Input: 61,
Result: "C",
}, {
Input: 42,
Result: "D",
}, {
Input: 20,
Result: "F",
},
}
for _, v := range scenarios {
grade := CalculateGrade(v.Input)
assert.Equal(t, v.Result, grade)
}
}
Then let’s check the coverage again
go test ./grade -coverprofile=cover.out && go tool cover -html=cover.out
So our test got 100% coverage and it means our test covers all the cases for the CalculateGrade
function.
To proof what I said earlier before about the benefit of unit testing
you will be able to catch any defects introduced due to the change you made in your code.
Let’s say if you or another developer accidentally changed the grade in the future.
...
if score <= 100 && score > 80 {
return "D"
}
...
if score <= 50 && score > 40 {
return "A"
}
...
You don’t want this to happen in your production app where the student whose got score 95 got grade D and whose got 44 got grade A. And if this really happen in production where you don’t have your unit test, you need time (maybe a long time depend on your code base size as in this example we use simple function) to trace which part of your code cause this error and you need some effort to fix the grades that already miss calculated and maybe already inserted in your database.
In addition, it may feel unnatural to leave files named addition_test.go in the middle of your package. Rest assured that the Go compiler and linker won’t ship your test files in any binaries it produces.