One of the first ever open source conferences I attended was YAPC::Eu in Amsterdam, in 2001. One talk that stuck in my mind was about testing, and from around that time I made a point of making sure that my work was adequately covered by unit tests. Perl was actually a very early adopter of unit tests, and not many people know that even Perl 1.0 in 1987 had a unit test suite. The Perl test modules in 2001 were already quite good at writing concise and expressive (though not necessarily well-organized) tests.
As such, it’s one of the first things I want to make sure I can do in
a new language. It seems that the
basic support for testing in go
has a few set of useful features, but also in other ways it’s quite
bare bones, leading to the existence of extensions. I’ve checked a
few out, and the one which most resembles the testing patterns I’ve
come to love is Testify. First I’ll show a basic unit test using the
standard library module, before giving a basic rundown on testify
.
Basic testing with go test
The good news is that all you have to do to write unit tests is to
drop functions in the appropriate path and location, and then go
test
will run those unit. The existence of this convention alone is
a fantastic start and lends itself towards comprehensive unit tests.
ie, unit test for bar.go
in package foo
should be written as
functions in bar_tests.go
, and named starting with Test
,
taking a t *testing.T
as their only argument, and no return value.
Test functions should return normally to pass. To fail, you can
either panic (but remember, don't panic!
), or call a method on the
testing.T
object to give a friendlier failure message. You can
also mark tests as skipped, and write messages to the error log.
There are versions of the functions which return immediately vs keep
going, and versions which log. I’ve attemped to summarize these
below:
Pass | Fail | Skip | |
---|---|---|---|
Silent, keep going | n/a | t.Fail() |
n/a |
Silent, stop test | return |
t.FailNow() |
t.SkipNow() |
Log, keep going | t.Log(...) |
t.Error(...) |
n/a |
Log with formatting, keep going | t.Logf(string, ...) |
t.Errorf(string, ...) |
n/a |
Log, stop test | n/a | t.Fatal(...) |
t.Skip(...) |
Log with formatting, stop test | n/a | t.Fatalf(string, ...) |
t.Skipf(string, ...) |
The “keep going” variants are not always found in other test systems, but they basically just mark the test as failed and allow it to continue. The idea behind this is that knowing that your change broke 100 tests is more useful than knowing it broke at least 1, and possibly more. The downside is that the first broken test may have caused the subsequent failures, making them “carried errors”.
A basic test script ends up looking like this:
package example
import "testing"
func TestSumFunc(t *testing.T) {
if SumFunc([]int{4, 5}) != 9 {
t.Fail()
}
}
When this fails, it looks like this:
$ go test
--- FAIL: TestSumFunc (0.00s)
FAIL
exit status 1
FAIL _/Users/samv/work/golang-scratch/example 0.006s
Success looks like this:
$ go test
PASS
ok _/Users/samv/work/golang-scratch/example 0.006s
Showing context with testify
assertions
The built-in testing module is simple enough to detect failures, but it’s helpful to know when things fail, exactly how it failed. This avoids you from having to debug your test script just to figure out what went wrong. A good test wraps all its failures with a message saying what was expected, what happened, and what the context of the failure was.
You could do this using intermediate variables, and error strings:
func TestSumFunc(t *testing.T) {
sum := SumFunc([]int{4, 5})
if sum != 9 {
t.Errorf("Expected 9, got %v", sum)
}
}
This error message at least gives you some information:
$ go test
--- FAIL: TestSumFunc (0.00s)
adder_test.go:8: Expected 9, got 1
FAIL
exit status 1
FAIL _/Users/samv/work/golang-scratch/example 0.006s
However, this is fiddly and requires a lot of extra lines. Enter
github.com/stretchr/testify
. This brings in the sort of test
assertion functions that programmers will be used to from Python’s
unittest, Perl’s Test::More, Ruby’s rspec, etc.
package example
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSumFunc(t *testing.T) {
assert.Equal(t, 9, SumFunc([]int{4, 5}), "Sum of 4 and 5")
}
Down to one line! And we get a better error, to boot:
$ go test
--- FAIL: TestSumFunc (0.00s)
Error Trace: adder_test.go:10
Error: Not equal: 9 (expected)
!= 1 (actual)
Messages: Sum of 4 and 5
FAIL
exit status 1
FAIL _/Users/samv/work/golang-scratch/example 0.008s
The last argument, the message, is optional but highly recommended. If you are repeating a test many times inside a loop for example, you can include the iteration number or details of the item being iterated on to give the person who has to deal with the failure a head start in seeing what went wrong. In other instances, the line number of the failure
The library supports a lot of test functions; some of the most useful are described below. I didn’t include the optional messages arguments, but they all take it so make sure to in your tests!
Example code | Purpose & notes |
---|---|
assert.Equal(t, expected, actual) |
Test for equality. Your testing staple. Be careful: literals
like 1 will mismatch types with for example int64(1) . Use
assert.NotEqual() carefully with that caveat. Put
the expected value first.
|
assert.Nil(t, val) |
For testing that values are equal to or equivalent to nil or not.
|
assert.NoError(t, err) |
This is the same as assert.Nil() , but prints a more
descriptive failure message. |
assert.Contains(t, coll, item) |
Checks whether the given item exists within the collection.
Checks for substrings, map keys or element presence, when passed
a string, map or slice for coll , respectively.
|
assert.IsType(t, expected, object) |
Test that the two types are the same; pass an empty object (eg,
&SomeType{} ) |
assert.Implements(t, expected, object) |
Test that the object implements the given type; pass a nil
interface object (eg, (*SomeInterface)(nil) ).
This is most useful when you are testing that a type you are
defining conforms to a third party interface, as normally
compilation itself will find cases where you are not implementing
an interface correctly.
|
assert.Panics(t, someFunc) |
Tests that the passed function panics; catching a panic normally involves an intermediate function, so this is highly convenient for simplifying these (generally very exceptional) error cases. |
This is just a small sampling! There are negated versions of many of
the above which start with Not
, as well as various assertions for
testing things like length, approximate equality for floating point
use, truth (assert.True
and assert.False
, of course); see the
complete documentation
for more.
Notably absent in current versions are range functions: things like
assert.Greater()
is nowhere to be found. For those, you would
have to fall back to the basic style of assertion.
Another thing to watch out for is that it’s quite easy to make your
test script itself panic when there are failures. These can be
annoying to unpack, so always test and guard subtests which
dereference returned values that might be nil
; assertions handily
return a boolean value you can use for this purpose:
x, err := SomeFunc(y, z)
info := []interface{}{"SomeFunc(y, z) with y, z = ", y, z}
assert.NoError(t, err, "y, z = ", info...)
if assert.NotNil(t, x, "y, z = ", info...) {
assert.Equal(t, "baz", x.Bar, info...)
}
Mocking objects using testify
mock objects
Go already has a built-in system for building mock objects: interfaces. Most testing libraries that implement mocking work by temporarily “monkey patching” functions and passing in dynamic objects which do nothing.
In go, you would typically define an interface that describes the object that your library deals with. Your test script would deliver a simple version that doesn’t do anything except perhaps collect information on its interaction with the code being tested.
This is already sufficient for most traditional uses of mocking. It’s also usually possible to convert your functions from ones that take concrete objects to ones that take interfaces. As a bonus, you’ll probably be making the boundary between your code and the other module more explicit and interchangable with alternate implementations.
You may be able to simplify the building of your test classes with
the testify/mock
library. It allows you to very quickly build out
test objects which collect which of these stubbed function calls are
used, as well as provide dynamic, per-test stuffing of results.
It works reasonably for this purpose.
Testing Coverage, Benchmarks, Race detection, Profiling, and more…
Go is the promised land of microoptimization, and so there is wide support in the testing toolset for a variety of powerful techniques you’ve been used to. More on this to come!