Recently I started dabbling in Go and inevitably reached the point to test what I had written. It turns out that Gos testing with its standard packages is as easy as it gets. The go
tool provides a test command to execute tests.
How to write a test?
It is very simple:
- Create a test file
- Write a test function (should start with
Test
) - Execute the tests
Create a file with a name matching the pattern *_test.go
The go test
command will compile the package and also all files matching the pattern. By default go test
needs no additional arguments and will compile all files in the current directory (without sub-dirs).
Write a test function
The signature as follows: func TestSomeFunctionality(t *testing.T)
. For example:
package main
import "testing"
func TestSomeFunctionality(t *testing.T) {
// your testing code goes here
}
Execute the tests
Just go into the package’s folder and execute go test
or for more details go test -v
(verbose).
A complete example
Let’s say that you have a package named packagetotest and a single file in it palindrome.go
:
package packagetotest
func isPalindrome(num int) bool {
if num < 0 {
num = -num
}
number := num
var n int
for number > 0 {
n = 10*n + number%10
number /= 10
}
return n == num
}
Create a file named palindrome_test.go
, it would work with any file ending at _test.go
, but I usually like to keep the test file names the same as the files they are testing. Here is a file with a very simple test:
package packagetotest
import "testing"
func TestIsPalindrome(t *testing.T) {
v := isPalindrome(123454321)
if v != true {
t.Error("Expected ", true, " got ", v)
}
}
Now execute go test
in the package folder. You should get something like the following output.
Note that an empty test function will PASS
.
PASS
ok example/testing 0.055s
Writing test functions for each value gets old pretty fast, so let’s define series of test values and expected results. Now our palindrome_test.go
looks like this:
package packagetotest
import "testing"
var tests = []struct {
n int // input
expected bool // expected result
}{
{1234, false},
{87698, false},
{1234321, true},
{1987891, true},
{-1234321, true},
{-8762822, false},
}
func TestIsPalindrome(t *testing.T) {
for _, tt := range tests {
v := isPalindrome(tt.n)
if v != tt.expected {
t.Error("Expected ", tt.expected, " got ", v)
}
}
}
This is a very common way to setup tests, with many examples in the go standard packages.
Benchmarks
Writing benchmarks is very similar to writing test functions. The function name starts with Benchmark
instead of Test
and the benchmark tests are not run when go test
is executed. In order to run the benchmarks, the go test
needs to include -bench
flag. To run all benchmarks and tests you can use go test -bench .
or go test -bench=.
.
Let’s write a benchmark function to test the IsPalindrome function and add it to palindrome_test.go
. Note the changed signature of the method.
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
isPalindrome(12345654321)
}
}
Executing go test -bench . -v
should give you a result similar to the one below:
> go test -bench . -v
=== RUN TestIsPalindrome
--- PASS: TestIsPalindrome (0.00s)
BenchmarkIsPalindrome-6 50000000 37.5 ns/op
PASS
ok example/testing 1.961s
Final Thoughts
Overall I really like that testing is given priority in go and I appreciate how easy is to use the testing package. There is simply no excuse not to test. Coming from different testing frameworks this may feel a little too vanilla. If you need more feature rich testing frameworks - there are many testing frameworks for go.
Coming from C++, there are features I wish were there. For example the missing comparisons for closeness of floating point numbers, asserts, etc. make the code longer, but it is not a big inconvenience for me.