Welcome to the twenty-ninth post of 52-technologies-in-2016 blog series. This week we will take our Go knowledge to the next level by learning how to perform unit testing in Go. Unit testing has become an essential skill set for every programmer. Unit testing is a software testing in which we test individual units of source code. Go has inbuilt support for unit testing. It has a testing
package that provides infrastructure to write unit tests. In this blog we will focus on writing test cases for a couple of programs we wrote in part 1.
Before you can start with this post make sure you have Go installed on your machine. Once you have Go installed, setup your Go workspace by following https://golang.org/doc/code.html article. This is the recommended way to setup your Go work directory.
After following the instructions mentioned in the article you will have a Go workspace directory like $HOME/dev/git/golang
. Inside the workspace directory, you will have src
,pkg
, and bin
directories inside the $HOME/dev/git/golang
. Inside the src
directory, create a directory structure as shown below.
$ mkdir -p src/github.com/shekhargulati
Note that $
is used to signify command-line prompt. You don't have to type $
.
Restart the terminal to make sure changes are picked. To check, you can run go env
which will list GOPATH
among other Go related environment variables. Below is the output of go env
command on my machine. These might be different for you depending on your operating system.
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/shekhargulati/dev/git/golang"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GO15VENDOREXPERIMENT="1"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common"
CXX="clang++"
CGO_ENABLED="1"
Once you have done the above mentioned setup, you should create a new directory called problems
inside the src/github.com/shekhargulati
directory and change directory to it.
$ mkdir problems && cd problems
The problems
directory will have the source code for this blog.
Let's get started with unit testing.
Let's start by writing test case for one of the example problems we discussed in part 1 EqualsOrNotEquals. The problem statement was as follows: Write a program that invokes a function that takes three integers as parameters and return true
if all of the three numbers are equal, otherwise it returns false
.
Let's start by writing a unit test for this program. In Go, to write a test case you have to create a file with name <program>_test.go
. Here, you have to replace <program>
with the name of your program i.e. equalornotequal
in our case. Only files that end with _test.go
will be considered for testing. Create a new file equalornotequal_test.go
inside the problems
directory. Copy and paste the content shown below in the equalornotequal_test.go
file.
package problems
import "testing"
func TestShouldReturnTrueWhenThreeNumbersAreEqual(t *testing.T) {
areThreeNumbersEqual := equalOrNotEqual(1, 1, 1)
if !areThreeNumbersEqual {
t.Error("Expected true, got", areThreeNumbersEqual)
}
}
Let's understand the unit test written above line by line.
-
The first statement defines package which will contain our tests. If you remember, in the first post we used package name as
main
for all our programs. When you have an executable program i.e. program which containsmain
method then you have to usemain
package. As we don't need executable program now, we have used package name asproblems
. -
Then, we defined an import statement which will import the
testing
package. Thetesting
package is provided by Go SDK. -
Then, we defined our test function
TestShouldReturnTrueWhenThreeNumbersAreEqual
. All tests should start withTest
string. This is the naming convention for Go test cases. Go will find all the exported functions that have name starting withTest
and run them. The test method accept a pointer of typetesting.T
. Thetesting.T
pointer provides support for reporting the output and status of each test. -
Inside the
TestShouldReturnTrueWhenThreeNumbersAreEqual
test case, we calledequalOrNotEqual
function passing it three 1's. We assigned result in a variableareThreeNumbersEqual
boolean variable. One thing you will note that there are no assertions. Later in the post we will use an assertion library. -
Finally, we checked is
areThreeNumbersEqual
is true if not then we callError
function passing it our message. If test return false, thent.Error
will be called which will report a test case failure.
To run the test case, go has a test command. Run the command shown below to test all the test cases inside the problems
directory.
$ go test
# _/Users/shekhargulati/dev/git/golang/src/github.com/shekhargulati/problems
./equalornotequal_test.go:7: undefined: equalOrNotEqual
FAIL _/Users/shekhargulati/dev/git/golang/src/github.com/shekhargulati/problems [build failed]
As expected, test fails because equalOrNotEqual
function is undefined as we have not written it yet.
Let's write code for equalOrNotEqual
function. Create a new file equalornotequal.go
inside the problems
directory and copy and paste the code shown below.
package problems
func equalOrNotEqual(first, second, third int) bool {
if first == second && second == third {
return true
} else {
return false
}
}
The code shown above creates a new function equalOrNotEqual
that checks if three integers are equal or not.
Now, run the test case again using the go test
command. This time test will pass as shown below.
$ go test
PASS
ok github.com/shekhargulati/problems 0.006s
By default, go will run all the test cases inside the current directory. If you want to run specific test cases then you can use -run
option passing it a regex matching test case names. Let's write one more test case that test scenario when numbers are not equal.
func TestShouldReturnFalseWhenThreeNumbersAreNotEqual(t *testing.T) {
areThreeNumbersEqual := equalOrNotEqual(1, 2, 3)
if areThreeNumbersEqual {
t.Error("Expected false, got", areThreeNumbersEqual)
}
}
If you run the test cases using go test
both the tests will pass.
To run only TestShouldReturnTrueWhenThreeNumbersAreEqual
test you can use -run
option as shown below.
$ go test -run TestShouldReturnTrueWhenThreeNumbersAreEqual
Rather than writing full name of a test you can pass a regex as well. For example, to run all the test cases that start with TestShouldReturnTrue
you can run following test case.
$ go test -run "TestShouldReturnTrue.*"
Before we move ahead there is one important Go feature that we have not discussed so far but that is very important for everyone to understand. Most of you would have noticed that the standard Go functions that we have used so far like Println
or Sort
or Error
all starts with capital letter. In Go, functions that start with capital letter are exported functions. This means these functions are public so you can use them in your program. All internal functions uses camel case naming convention and are not accessible outside the file. So, if you try to access equalOrNotEqual
function from another package then you will get a compilation error.
package main
import (
"fmt"
"github.com/shekhargulati/problems"
)
func main() {
fmt.Println(problems.equalOrNotEqual(1, 1, 1))
}
When you will run run the code shown above you will get following compilation errors.
./a.go:9: cannot refer to unexported name problems.equalOrNotEqual
./a.go:9: undefined: problems.equalOrNotEqual
Keep in mind that only functions which starts with a capital letter are exported.
Rename equalOrNotEqual
function to EqualOrNotEqual
as this should be exported function.
Go supports both black box and white box testing. The test that we wrote previously is white box testing as we have access to internal details i.e. private members of a package. Go recommends that you should have tests in the same directory and package allowing you to access the internals. I feel this will lead to tests that don't test the behavior but implementation details. So, I personal like black box testing where I work against the exported functions only.
To perform black box testing, create a test file like we did previously equalornotequal.go
and copy and paste the following contents.
package problems_test
import (
"testing"
. "github.com/shekhargulati/problems"
)
func TestThreeEqualNumbers(t *testing.T) {
isThreeNumbersEqual := EqualOrNotEqual(1, 1, 1)
if !isThreeNumbersEqual {
t.Error("\tExpected true, got ", isThreeNumbersEqual)
}
}
There are couple of important changes to note.
-
We have used a different package name
problems_test
instead orproblems
. This means we will have access to only exported functions. -
In the import statement, we have to import our package
github.com/shekhargulati/problems
. Also, we useddot-import
so that exported functions are in theproblems_test
package scope.
You can improve the test readability by adding log statements. The testing.T
pointer has a lot of logging methods that you can use to make your test output more readable and understandable. Below shown is one such attempt.
func TestThreeEqualNumbers(t *testing.T) {
t.Log("Given three numbers are equal")
t.Logf("\tWhen we make a call to EqualOrNotEqual(%d,%d,%d)",1,1,1)
isThreeNumbersEqual := EqualOrNotEqual(1, 1, 1)
if isThreeNumbersEqual {
t.Log("\tThen we should get",isThreeNumbersEqual)
}else{
t.Error("\tExpected true, got ", isThreeNumbersEqual)
}
}
When you will run the go test command now with -v
option you will see a much better test output. Please note you have to run tests in verbose mode to see the log messages. If you remote -v
option, your tests will not print log statements.
$ go test -v -run TestThreeEqualNumbers
=== RUN TestThreeEqualNumbers
--- PASS: TestThreeEqualNumbers (0.00s)
equalornotequalblack_test.go:9: Given three numbers are equal
equalornotequalblack_test.go:10: When we make a call to EqualOrNotEqual(1,1,1)
equalornotequalblack_test.go:13: Then we should get true
PASS
ok github.com/shekhargulati/problems 0.007s
There are times when you would like to run a test case multiple times. We all have seen flaky tests which run most of the times but fail few times. It is very difficult to reproduce failing flaky tests. The only solution is to run test multiple times. go test
command allows you to specify the count of times you want to run a test as shown below.
$ go test -v -run TestThreeEqualNumbers -count 3
=== RUN TestThreeEqualNumbers
--- PASS: TestThreeEqualNumbers (0.00s)
equalornotequalblack_test.go:9: Given three numbers are equal
equalornotequalblack_test.go:10: When we make a call to EqualOrNotEqual(1,1,1)
equalornotequalblack_test.go:13: Then we should get true
=== RUN TestThreeEqualNumbers
--- PASS: TestThreeEqualNumbers (0.00s)
equalornotequalblack_test.go:9: Given three numbers are equal
equalornotequalblack_test.go:10: When we make a call to EqualOrNotEqual(1,1,1)
equalornotequalblack_test.go:13: Then we should get true
=== RUN TestThreeEqualNumbers
--- PASS: TestThreeEqualNumbers (0.00s)
equalornotequalblack_test.go:9: Given three numbers are equal
equalornotequalblack_test.go:10: When we make a call to EqualOrNotEqual(1,1,1)
equalornotequalblack_test.go:13: Then we should get true
PASS
ok github.com/shekhargulati/problems 0.006s
There are many other options provided by go test
command. You can look at all the options by running the go test --help
command.
If you have written tests in any programming language one thing that you will miss is an assertion package. Go SDK does not provide any assertion package but there are many community contributed assertion package. One such popular package is testify
. You can get the package by using the go get
command as shown below.
$ go get github.com/stretchr/testify
This will put the package in the pkg
directory inside the $GOPATH
. This is where all packages will be installed.
Now, you can improve your test case by writing assertions as shown below.
package problems
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestShouldAssertThatFunctionReturnsFalseWhenThreeNumbersAreNotEqual(t *testing.T) {
isThreeNumbersEqual := EqualOrNotEqual(1, 2, 3)
assert.False(t, isThreeNumbersEqual, "Expected false got true")
}
You can learn more about this package by reading its documentation.
Another cool feature provided by Go testing
package is table tests. Table test allows you to write a test once and run it against a table of data. Table will contain the input and expected output. Let's write test for closestpair
program. Given n numbers, find a pair which is closest to each other. For example, given 10, 6, 2, 5 numbers 5,6 is the closest pair.
One possible solution is shown below.
package problems
import (
"math"
"sort"
)
type Pair struct {
First, Second int
}
func ClosestPair(numbers []int) Pair {
sort.Ints(numbers)
var pair Pair
var diff int = math.MaxInt32
for index := 0; index < len(numbers)-1; index++ {
cur := numbers[index]
next := numbers[index+1]
if next-cur < diff {
diff = next - cur
pair = Pair{cur, next}
}
}
return pair
}
To write table tests, we will create a table closestPairTests
. It is an array of struct with two fields input array and output Pair. We populate the array with the input and output as shown below.
package problems_test
import (
. "github.com/shekhargulati/problems"
"github.com/stretchr/testify/assert"
"testing"
)
var closestPairTests = []struct {
in []int
out Pair
}{
{[]int{2, 10, 5, 6, 15}, Pair{5, 6}},
{[]int{2, 4, 5}, Pair{4, 5}},
{[]int{100, 5, 7, 99, 11}, Pair{99, 100}},
}
func TestClosestPair(t *testing.T) {
for _, tt := range closestPairTests {
t.Log("Running test for input", tt.in)
pair := ClosestPair(tt.in)
assert.Equal(t, tt.out.First, pair.First)
assert.Equal(t, tt.out.Second, pair.Second)
}
}
When we run the test case, we iterate over table entries running test for each input and asserting it against expected output.
When you will run the test case, you will see the following output.
$ go test -v -run TestClosestPair
=== RUN TestClosestPair
--- PASS: TestClosestPair (0.00s)
closestpair_test.go:20: Running test for input [2 10 5 6 15]
closestpair_test.go:20: Running test for input [2 4 5]
closestpair_test.go:20: Running test for input [100 5 7 99 11]
PASS
ok github.com/shekhargulati/problems 0.009s
That's all for this week. Please provide your valuable feedback by adding a comment to shekhargulati#42.