Basic testing with Go

· Read in about 4 min · (651 Words)
Go

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:

  1. Create a test file
  2. Write a test function (should start with Test)
  3. 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.