Writing good unit tests is very important not only to show that your code works while you write it, but also to ensure that future updates to your code do not break existing functionality.
There's a lot of bibliography on what is a good test, or when tests should be written, so I will not cover any of those here. Instead we will simply see how tests are written in Go.
In order to run all the tests in an existing package we can use the test
subcommand in the go
tool. For instance, to run all the tests in the
strings
package of the standard library we can run:
$ go test strings
ok strings 0.290s
You can add the -v
flag in order to list all of the tests that are run,
rather than only those that failed.
Let's now see how to write new tests.
Unit tests are functions that match the following conditions:
- they are defined in a file with name ending in
_test.go
- the function name starts with
Test
- the function is of type
func(t *testing.T)
In a previous chapter we
discussed file name suffixes, and how a file named foo_windows.go
will
only be compiled for Windows systems.
The same idea applies to the _test
suffix: any file containing that
suffix will be ignored unless we're running go test
.
The value received by a test, of type *testing.T
provides a series of
methods that allows us to flag when a test is failing:
- Use
Error
andErrorf
to indicate the test failed and continue executing it - Use
Fatal
andFatalf
to indicate the test failed and should not continue
You have two options depending on what kind of tests you want
to write for a given package sum
in the directory sum.
Given a file sum.go
:
package sum
// All returns the sum of the given values.
func All(vs ...int) int {
return recursive(vs)
}
func recursive(vs []int) int {
if len(vs) == 0 {
return 0
}
return vs[0] + recursive(vs[1:])
}
You can define a unit test for recursive
by creating a new
function in test file that is part of the same package sum
.
package sum
import "testing"
func TestRecursive(t *testing.T) {
// Implement the body of this test, calling recursive.
}
This is possible because the test belongs to the same package,
so even though recursive
is not exported it is still
visible to other members of the same package.
Another option, if you want to test only exported elements,
is to define a sum_test
package:
package sum_test
import "testing"
func TestAll(t *testing.T) {
// Implement the body of this test, calling sum.All.
}
As you can see, on this case we need to import the sum
package, as you would do if you were a user of the package.
Edit sum_internal_test.go and
sum_test.go in order to make sure the
implementation of All
and recursive
is correct.
Run go test
once you've written them.
Very often you will find yourself writing repetitive tests, let's imagine that you
were writing a test for strings.Index
, you might end up writing something like:
func TestIndex_Repeat(t *testing.T) {
if i := strings.Index("hello, world", "hello"); i != 0 {
t.Errorf(`"hello, world" should contain "hello" at position 0, not %d`, i)
}
if i := strings.Index("hello, world", "bye"); i != -1 {
t.Errorf(`"hello, world" should not contain "bye"`)
}
if i := strings.Index("hello, world", "world"); i != 7 {
t.Errorf(`"hello, world" should contain "world" at position 7, not %d`, i)
}
}
There's a lot of repeated code, and repeated code means errors can be introduced while copy pasting very easily.
In Go, it is recommended to use what we call a table drive test. Rather than repeating the code, we keep all the commonalities across all the repetitions and extract the differences into a slice of test cases. The previous example would be something like:
func TestIndex_Table(t *testing.T) {
tt := []struct {
text string
sub string
idx int
}{
{"hello, world", "hello", 0},
{"hello, world", "bye", -1},
{"hello, world", "world", 7},
}
for _, tc := range tt {
if idx := strings.Index(tc.text, tc.sub); idx != tc.idx {
if tc.idx >= 0 {
t.Errorf("%s should contain %s at position %d, not %d", tc.text, tc.sub, tc.idx, idx)
} else {
t.Errorf("%s should not contain %s", tc.text, tc.sub)
}
}
}
}
Even better, since Go 1.7, we can use subtests, which allow you to have a better control over what
test cases inside of a test you run. You need to give a name to your test case, which is a good
practice anyway, and call t.Run
.
func TestIndex_Subtest(t *testing.T) {
tt := []struct {
name string
text string
sub string
idx int
}{
{"first character", "hello, world", "hello", 0},
{"not found", "hello, world", "bye", -1},
{"last character", "hello, world", "world", 7},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
if idx := strings.Index(tc.text, tc.sub); idx != tc.idx {
if tc.idx >= 0 {
t.Fatalf("%s should contain %s at position %d, not %d", tc.text, tc.sub, tc.idx, idx)
}
t.Fatalf("%s should not contain %s", tc.text, tc.sub)
}
})
}
}
Note: you can use Fatalf
with subtests and only the current subtest will fail, unlike in the
previous loops where later tests would be ignored.
If you run go test -v
you'll see the subtest that were ran.
$ go test -v
--- PASS: TestIndex_Subtest (0.00s)
--- PASS: TestIndex_Subtest/first_character (0.00s)
--- PASS: TestIndex_Subtest/not_found (0.00s)
--- PASS: TestIndex_Subtest/last_character (0.00s)
Make your previous test even better by adding more use cases via subtests.
When you write tests in Go using the _test
packages, you
are somehow showing how to use the package. This is so
useful that Go provides a way to convert those tests into
examples.
Examples are functions whose name starts with Example
and
do not receive any parameters, nor return any values.
The name of an example contains the information of what the example is demoing. In order to write an example for:
- a function
Foo
, writeExampleFoo
, - a method
Foo
on typeBar
, writeExampleFooBar
, - a type
Foo
, writeExampleFoo
During tests we can verify the results by writing code, in examples we have a more limited solution: by checking the expected output of executing the example.
To do this, we simply add a comment of the form:
func ExampleHelloWorld() {
fmt.Println("hello, world")
// Output:
// hello, world
}
Write an example for Sum
and see it running on your local godoc
web
server.
Use an Output
comment to make sure that if you run go test
and the
output doesn't match the example will fail.
In order to test an http.Handler
or an http.HandlerFunc
we need to provide
an http.Request
and an http.ResponseWriter
. The first one is easy to create,
but the second one is a bit trickier.
For a slightly more advanced exercise try to write a test for the handler
function defined in our webserver program.
You're now able to write unit tests and examples, and you're able to run them on any package you wish. That's pretty awesome!
Next we're going to learn how to figure out what parts of your code you should be testing with code coverage.