Testing in Go: go test
Table of Contents
If you remember anything from this article, remember this: go test
is a
command that automates the execution of test files and functions in a Go
project. The go test
command ships with Go itself, so if you have Golang
installed, there’s nothing to check - it’s available on your machine.
go test
will recompile each package and any files with names matching the
file pattern *_test.go
. These *_test.go
files can contain test functions,
benchmark functions, and example functions. Each listed package will cause the
execution of a separate test binary.
go test
compiles test files that declare a package, ending with the suffix
*_test.go
as a separate package. It then links them with the main test binary
and runs them.
While go test
seems simple on the surface, it has many options and commands.
It allows for control of what test to be run, such as file-specific or
package-specific tests. It has flags for skipping tests, reporting test
coverage, and other options. Also, it has intelligent caching mechanisms under
the hood to avoid rebuilding packages every time we run our tests.
go test
is a compact yet a rich tool that every Gopher should have under
their belt. We will not look at all its features, but at the end of this
article, we will have a good understanding of the workings of the tool.
Running modes #
go test
has two running modes. Understanding them is essential to have an
easy time working with the tool:
- Local directory mode, or running without arguments
- Package list mode, or running with arguments
In the local directory mode, go test
compiles the package sources and
tests found in the current directory and then runs the resulting test binary.
This mode disables caching. After the package test finishes, go test
prints a
summary line showing the test status (‘ok’ or ‘FAIL’), the package name, and
elapsed time.
To run your tests in this mode, run go test
in your project’s root directory.
In the package list mode, go test
compiles and tests each package listed
as arguments to the command. If a package test passes, go test
prints only
the final ‘ok’ summary line. If a package test fails, go test
prints the
complete test output.
To run your test in this mode, run go test
with explicit package arguments.
For example, we can run go test PACKAGE_NAME
to test a specific package or
go test ./...
to test all packages in a directory tree. Or we can run go test .
to run all tests in the current directory.
In our daily work with go test
, the difference between the two modes is
caching.
When we run go test
in package list mode, it will cache successful package
test results to avoid unnecessary reruns. When it can find a test result in the
cache, go test
will redisplay the cached result instead of rerunning the
tests. When this happens, go test
will annotate the test results with
(cached)
in place of the elapsed time in the summary line.
Test run control #
One of the significant features of the go test
command is controlling what
test files and test functions we can run. As we discussed before, go test
has
two modes to supply package or file names. Additionally, we can also
selectively run one or more test functions.
To have an easier time further in this post, we will define a type Person
.
It will have two functions: a constructor NewPerson
and a function older
,
which will take a *Person
and return if one *Person
is more senior than
another *Person
.
package person
import "errors"
type Person struct {
age int
}
func NewPerson(age int) (*Person, error) {
if age < 1 {
return nil, errors.New("A person is at least 1 years old")
}
return &Person{
age: age,
}, nil
}
func (p *Person) older(other *Person) bool {
return p.age > other.age
}
Let’s add tests for the constructor and the older
function. We want to test
that the constructor will return an error when a negative integer is passed as
the age
argument:
package person
import (
"testing"
)
func TestNewPersonPositiveAge(t *testing.T) {
_, err := NewPerson(1)
if err != nil {
t.Errorf("Expected person, received %v", err)
}
}
func TestNewPersonNegativeAge(t *testing.T) {
p, err := NewPerson(-1)
if err == nil {
t.Errorf("Expected error, received %v", p)
}
}
Let’s add two more tests for the older
function. We will create two *Person
with different ages and check for the return value of the older
function:
func TestOlderFirstOlderThanSecond(t *testing.T) {
p1, _ := NewPerson(1)
p2, _ := NewPerson(2)
if p1.older(p2) {
t.Errorf("Expected p1 with age %d to be younger than p2 with age %d", p1.age, p2.age)
}
}
func TestOlderSecondOlderThanFirst(t *testing.T) {
p1, _ := NewPerson(2)
p2, _ := NewPerson(1)
if !p1.older(p2) {
t.Errorf("Expected p1 with age %d to be older than p2 with age %d", p1.age, p2.age)
}
}
The first way to control test runs is by supplying the test files as arguments
to the go test
command. For example, if we have a person.go
and
person_test.go
files, we need to run:
$ go test person.go person_test.go
ok command-line-arguments 0.005s
The go test
command prints only the final ok
because all the test cases
passed. If we would like to see a more detailed output, we can use the -v
flag:
$ go test person.go person_test.go -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- PASS: TestOlderFirstOlderThanSecond (0.00s)
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
ok command-line-arguments 0.005s
The second way to run the test for the person
package is to supply its name
to the go test
command:
$ go test person
ok person 0.004s
This way, the go test
locates the package, builds it, and runs its tests.
(Adding the -v
flag will produce the same output as before.)
The third way to run tests is by specifying a test function to be run, using
the -run
flag. The flag takes an argument, a regex that will try to match
against any test functions in the current directory. For example, if we would
like to run the TestNewPersonPositiveAge
and TestNewPersonNegativeAge
function, we can do this:
$ go test -run TestNewPerson -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
PASS
ok person 0.004s
If we mistakenly supply a regexp that will not match anything, go test
will
inform us about it:
$ go test -run TestFoo -v
testing: warning: no tests to run
PASS
ok person 0.004s
Failing Fast #
When running tests, we often want to lower the waiting time and stop at the first test failure we hit. The default mode is to wait for all tests to finish before reporting the errors. So this can be a helpful feature, especially for larger projects.
To do this, go test
has a special flag we can use: -failfast
. Fail fast
does what says on the tin: it will stop at the first test that fails, aborting
the running test suite/files. To see it in action, we will break one of the
test functions for the older
function that we introduced earlier:
// Removing everything else for brewity...
func TestOlderFirstOlderThanSecond(t *testing.T) {
p1, _ := NewPerson(100)
p2, _ := NewPerson(2)
if p1.older(p2) {
t.Errorf("Expected p1 with age %d to be younger than p2 with age %d", p1.age, p2.age)
}
}
We know this test will fail: p1
will be older than p2
because they are
100
and 2
years old, respectively. If we run go test
, it will run all the
test functions, ignoring when some fail:
$ go test -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- FAIL: TestOlderFirstOlderThanSecond (0.00s)
person_test.go:26: Expected p1 with age 100 to be younger than p2 with age 2
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
FAIL
exit status 1
FAIL person 0.004s
Now, let’s see the difference if we add the -failfast
flag:
$ go test -v -failfast
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- FAIL: TestOlderFirstOlderThanSecond (0.00s)
person_test.go:26: Expected p1 with age 100 to be younger than p2 with age 2
FAIL
exit status 1
FAIL person 0.004s
By adding the -failfast
flag, we stopped right where we got the first failure
- when running the
TestOlderFirstOlderThanSecond
test function.
This option is useful when we do some heavier refactorings of our code. If our
refactor breaks some tests, instead of being overwhelmed by all the errors, we
can add the -failfast
flag and fix our mistakes one by one.
Next time you do some big refactors, give it a shot; it might save you a few grey hairs as you’re fixing the tests.
Coverage #
Another exciting feature that is packed in go test
is test coverage. Taken
from Go’s website from the blog post on
coverage:
Test coverage is a term that describes how much of a package’s code is exercised by running the package’s tests. If executing the test suite causes 80% of the package’s source statements to be run, we say that the test coverage is 80%.
We will not go into detail here on how the coverage tools work. I recommend referring to the “Test coverage for Go” section in the post above to understand how it measures the coverage.
If we would like to check the test coverage on our Person
type and its
functions, we can run go test -cover
:
$ go test -cover
PASS
coverage: 100.0% of statements
ok person 0.004s
100% coverage is excellent - it means that our tests cover all the
functionality of our program. We can add some more functionality to the
NewPerson
constructor function to see the coverage tool in action. Creating a
person that is 1000 years old doesn’t make much sense, especially
knowing the
older person ever recorded was 122 years old. We can extend the constructor to
validate that the newly created Person
instance cannot be older than 130
years:
package person
import "errors"
type Person struct {
age int
}
func NewPerson(age int) (*Person, error) {
if age < 1 {
return nil, errors.New("A person is at least 1 years old")
}
if age >= 130 {
return nil, errors.New("A person cannot be older than 130 years")
}
return &Person{
age: age,
}, nil
}
func (p *Person) older(other *Person) bool {
return p.age > other.age
}
If we rerun the coverage report, we will see the following output:
$ go test -cover -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- PASS: TestOlderFirstOlderThanSecond (0.00s)
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
coverage: 83.3% of statements
ok person 0.004s
Whoops, our coverage dropped from 100% to 83.3%. To see where our problem is,
we can ask go test
to create a coverage profile for us, using the
-coverprofile
flag:
$ go test -coverprofile=prof.out
PASS
coverage: 83.3% of statements
ok person 0.004s
The output is a prof.out
file, with the following contents:
$ cat prof.out
mode: set
person/person.go:9.42,10.13 1 1
person/person.go:14.2,14.16 1 1
person/person.go:18.2,20.3 1 1
person/person.go:10.13,12.3 1 1
person/person.go:14.16,16.3 1 0
person/person.go:23.44,25.2 1 1
We don’t have to understand the output here, as its format is not
user-friendly. Go has another tool that we can use with the profile file to
visualize the coverage report better: go tool cover
. Using the prof.out
profile file, we can generate an HTML page that will visualize what tests cover
which parts of the code:
$ go tool cover -html=prof.out
This command pops open our browser with a page looking like this:
If we look at the image above, the red-colored code is the one that is never exercised by our tests. In other words, the red code is not covered, while the green one is. The grey code is not tracked because it’s boilerplate and should not be tested from the coverage tool perspective.
From the image, it’s clear that our tests never run the new branch where we handle unreasonably old age as the argument. Let’s add another quick test function that will cover this functionality:
// person_test.go
func TestNewPersonHugeAge(t *testing.T) {
p, err := NewPerson(150)
if err == nil {
t.Errorf("Expected error, received %v", p)
}
}
After running go test -coverprofile=hundred.out -v
we will see the coverage
go back to 100% again:
$ go test -coverprofile=hundred.out -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestNewPersonHugeAge
--- PASS: TestNewPersonHugeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- PASS: TestOlderFirstOlderThanSecond (0.00s)
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
coverage: 100.0% of statements
ok person 0.004s
Here’s the HTML output of the coverage profile:
The go test
and go tool cover
tools packs more combined functionality. I
encourage you to read more about it in the official blog
post and to experiment further with the tools.
Running modes #
Software is built to serve highly mutable business requirements, which results in applications that grow and evolve. Such applications are (hopefully) well tested, with tests operating on unit, integration and end-to-end levels.
A common technique on larger projects is to be explicit about what type of
tests we want to run. For example, when running go test
, do we want to
include running the integration tests of the project or just the unit tests? In
such cases, two practical techniques come out of the box with Go: build tags
and short mode.
Using -short
mode #
The -short
mode for go test
allows us to mark any long-running tests to be
skipped in this mode. go test
and the testing
package support this via the
t.Skip()
, the testing.Short()
functions and the -short
flag.
We can skip a test function by checking if the short mode is on and invoking
the t.Skip
function if that returns true
. Let’s see this by changing an
earlier example:
func TestOlderFirstOlderThanSecond(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long-running test.")
}
p1, _ := NewPerson(1)
p2, _ := NewPerson(2)
if p1.older(p2) {
t.Errorf("Expected p1 with age %d to be younger than p2 with age %d", p1.age, p2.age)
}
}
If we run the tests using go test -v
, we should see no change in the output:
$ go test -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestNewPersonHugeAge
--- PASS: TestNewPersonHugeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- PASS: TestOlderFirstOlderThanSecond (0.00s)
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
ok person 0.004s
If we would enable the short mode using the -short
flag, we should see the
change in the behavior and output:
$ go test -short -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestNewPersonHugeAge
--- PASS: TestNewPersonHugeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- SKIP: TestOlderFirstOlderThanSecond (0.00s)
person_test.go:30: Skipping long-running test.
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
ok person 0.005s
We can see that the output contains one SKIP
line, meaning in short mode, go test
skipped the test function. We can use -short
in other ways, besides
skipping a test. For example, mocking network calls instead of opening a
connection or loading simple fixtures instead of loading them from a database.
The options are many; it all depends on the function that the test is covering.
Using build tags #
go test
supports build tags out of the box, like the -short
flag. While we
apply the short mode on the function level, we use the build tags on a file
level. The nature of build tags does not allow us to use them to skip one or a
few functions. Instead, build tags are used to run (or skip) a particular type
of test file from our test suite.
There are a couple of rules on build tags. As indicated in this Stack Overflow answer:
- build tags are special comments, with the format
// +build TAGNAME
- we have to place the tag on the first line of the file
- we have to add an empty line after the tag
- the tag name comment cannot have a dash, but it allows underscores
Following these rules, we can add a tag to our person_test.go
file from
earlier:
// +build person_tests
package person
import (
"testing"
)
// Snipped...
(The person_tests
tag is useless when we have only one file in the test
suite, but we will add it for experimentation purposes.)
To add the build tag to our go test
command, we need to run go test -tags=TAG_NAME
. To run the files with the person_tests
build tag:
$ go test -tags=person_tests -v
=== RUN TestNewPersonPositiveAge
--- PASS: TestNewPersonPositiveAge (0.00s)
=== RUN TestNewPersonNegativeAge
--- PASS: TestNewPersonNegativeAge (0.00s)
=== RUN TestNewPersonHugeAge
--- PASS: TestNewPersonHugeAge (0.00s)
=== RUN TestOlderFirstOlderThanSecond
--- PASS: TestOlderFirstOlderThanSecond (0.00s)
=== RUN TestOlderSecondOlderThanFirst
--- PASS: TestOlderSecondOlderThanFirst (0.00s)
PASS
ok person 0.005s
Based on the supplied tag, go test
detects the files tagged with
person_tests
and runs them. If we provided a tag that does not exist in the
package, the output would report that no files are found:
$ go test -tags=foo -v
? person [no test files]
Notable mentions #
Covering all the features that go test
packs in a single post is a hard
undertaking. Here I will mention some other useful features that you should try
to use and read more on:
- List tests with
-list
: to list tests, benchmarks, or examples matching a regular expression passed as the argument. The-list
flags are analogous to the-run
flag that we discussed before. When we use this flag,go test
will not run any tests, benchmarks, or examples. - Disabling caching with
-count
: as discussed before,go test
by default caches test results for packages. It then uses them to skip running tests that have are not modified between two runs. Although it can improve the performance, there are times when we would like to disable the caching using the-count=1
flag. - Get JSON output using
-json
: if we would like to take thego test
output and process it using a program, having it in a JSON format is better than plain text. This flag will convert the output to JSON, so processing it with a program is less cumbersome. - Use more CPU cores using
-cpu
: this flag will set theGOMAXPROCS
variable to the argument that we pass to the flag. It limits the number of operating system threads that can execute user-level Go code simultaneously. - Detect race conditions using
-race
: since Go provides concurrent code primitives, race conditions are always a risk. Go’s tooling is great in this regard - it has a fully integrated race conditions detector in its toolchain. You can read more about it in its announcement blog post.