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
TestOlderFirstOlderThanSecondtest 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-listflags are analogous to the-runflag that we discussed before. When we use this flag,go testwill not run any tests, benchmarks, or examples. - Disabling caching with
-count: as discussed before,go testby 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=1flag. - Get JSON output using
-json: if we would like to take thego testoutput 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 theGOMAXPROCSvariable 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.