As I was going down the stairs I remember feeling the temperature dropping. It was an astounding experience descending towards the basement in my grandparents’ house – it felt cooler and quieter. The light was warm and dimmed, with creepy shadows dropping behind the old furniture that was serving its last years before getting disposed to the city cleaning service.

One of the rooms in the basement was my grandfather’s workshop. There were many tools that he collected over the years. Some were hanging on the wall, making it quite hard for me to reach them. The ones on the working desk were easy to reach, yet very hard to carry. At least for 5-year-olds.

It was one of the first times I remember trying to handle any tooling. Watching my grandfather working with tools was always great because I could see something get created in front of my eyes. I remember him making me a pair of nunchucks (not a good idea!) A mundane task for him, but I was watching a magician conjure a pair of home-made nunchucks out of some wood and a very light chain. Very cool!

Years later I realised that knowing how to use my tools to create things out of thin air is one of my great joys. Programmers are also builders, so we have to know our tools well. We are not making physical masterpieces (nunchuks anyone?), but we sure have to learn our tools to build good software.

That’s why in this article we will look at one important tool from our Go toolbox - the go test command.

What is go test anyway?

If you’re going to remember anything, remember this: go test is a command which automates the execution of test files and functions in a Go project. The go test command ships with Go itself, so if you have Go installed there’s nothing to check - it’s available on your machine.

go test will recompile each package along with 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 actually has many options and commands. It allows for control of what test to be run, as in file-specific or package-specific tests, has flags for skipping tests, reporting test coverage and other options. Also, it has smart caching mechanisms under the hood, to avoids rebuilding packages every time we run our tests.

go test is a compact yet a rich tool that every Gopher should have a solid command of. 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 having an easy time working with the tool:

  1. Local directory mode, or running without arguments
  2. 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 of the packages 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 full 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 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 the result of a test in the cache, go test will redisplay the cached result instead of running the tests again. When this happens, go test will annotate the test results with (cached) in place of the elapsed time in the summary line.

Join The Newsletter

I write about backend technologies, programming and cloud architectures. Join hundreds of other developers that get my content, twice a month. Unsubscribe whenever. Never any spam, ads, or affiliate links.

You can also subscribe via RSS.

Test run control

One of the major features of the go test command is the ability to control what tests files and test functions we can run. As we discussed before, go test has two modes to which we can 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 older than another *Person.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
	}
}

(If you find the tests a bit confusing I apologise - tried hard to make them more readable but it was hard. I guess that says something about the example.)

The first way to control tests running 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, which is 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

Often when running tests we 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 the errors are reported, so depending on the project this can save us some time.

To do this, go test has a special flag we can use: -failfast. Fail fast does what it advertises: it will stop at the first test that fails, exiting the process that is running the test suite/files. To see it in action, we will break one of the test functions for the older function that we introduced earlier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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, regardless if 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 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 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 actually 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 errors one by one.

Next time you do some big refactors give it a shot, it might save you some sanity as you’re fixing the tests.

Join The Newsletter

I write about backend technologies, programming and cloud architectures. Join hundreds of other developers that get my content, twice a month. Unsubscribe whenever. Never any spam, ads, or affiliate links.

You can also subscribe via RSS.

Coverage

Another interesting 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 in detail here on how the coverage tools work. I recommend referring to the “Test coverage for Go” section in the aforementioned post to understand how it actually 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

This is great - it means that our tests cover all the functionality of our functions. To see the coverage tool in action, we can add some more functionality to the NewPerson constructor function. Creating a person that is 1000 years old doesn’t make much sense, especially knowing that 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 would run the coverage report again, 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

This will create a prof.out file, looking like this:

$ 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, because Go has another tool that we can use with the profile file to visualise the coverage report better: its name is go tool cover. Using the prof.out profile file we have, we can generate an HTML page that will visualise what parts of the code are covered by tests:

$ go tool cover -html=prof.out

This command pops open our browser with a page looking like this:

Looking at the image above, the red coloured code is the one that is never run. In other words, the red code is not covered, while the green one is. The grey code is not tracked, because from the coverage tool perspective it’s boilerplate (read: not needed to be tested).

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:

1
2
3
4
5
6
7
// 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 expriment further with the tools.

Running modes

Software is built to serve highly mutable business requirements, which results in applications that grow and evolve over time. Such applications are (hopefully) well tested, with tests operating on unit, integration and end-to-end levels.

A common technique on bigger projects often is to be explicit 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, there are two useful techniques that 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 normally, 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 behaviour 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 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 networks 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. This means that there is no way to use build tags to skip one or a few functions. Rather, build tags are used to run (or skip) a special type of test files 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:

1
2
3
4
5
6
7
8
9
// +build person_tests

package person

import (
	"testing"
)

// Snipped...

(The person_tests tag is a useless when we have only one file in the test suite, but we will add it for experimentational 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 would supply 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 usage of the -list flags is 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 we 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 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 the output of go test and process it using a program, having it in a JSON format is better than normal 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 the GOMAXPROCS 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 it’s very easy to write concurrent code in Go, 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.
Join The Newsletter

I write about backend technologies, programming and cloud architectures. Join hundreds of other developers that get my content, twice a month. Unsubscribe whenever. Never any spam, ads, or affiliate links.

You can also subscribe via RSS.