Test structure
source ↗Test structure
Subtests
The standard Go testing library offers a facility to define subtests. This allows flexibility in setup and cleanup, controlling parallelism, and test filtering. Subtests can be useful (particularly for table-driven tests), but using them is not mandatory. See also the Go blog post about subtests.
Subtests should not depend on the execution of other cases for success or
initial state, because subtests are expected to be able to be run individually
with using go test -run flags or with Bazel test filter expressions.
Subtest names
Name your subtest such that it is readable in test output and useful on the
command line for users of test filtering. When you use t.Run to create a
subtest, the first argument is used as a descriptive name for the test. To
ensure that test results are legible to humans reading the logs, choose subtest
names that will remain useful and readable after escaping. Think of subtest
names more like a function identifier than a prose description.
The test runner replaces spaces with underscores, and escapes non-printing characters. To ensure accurate correlation between test logs and source code, it is recommended to avoid using these characters in subtest names.
If your test data benefits from a longer description, consider putting the
description in a separate field (perhaps to be printed using t.Log or
alongside failure messages).
Subtests may be run individually using flags to the Go test runner or Bazel test filter, so choose descriptive names that are also easy to type.
Warning: Slash characters are particularly unfriendly in subtest names, since they have special meaning for test filters.
# Bad: # Assuming TestTime and t.Run("America/New_York", ...) bazel test :mytest --test_filter="Time/New_York" # Runs nothing! bazel test :mytest --test_filter="Time//New_York" # Correct, but awkward.
To identify the inputs of the function, include them in the test’s failure messages, where they won’t be escaped by the test runner.
// Good:
func TestTranslate(t *testing.T) {
data := []struct {
name, desc, srcLang, dstLang, srcText, wantDstText string
}{
{
name: "hu=en_bug-1234",
desc: "regression test following bug 1234. contact: cleese",
srcLang: "hu",
srcText: "cigarettát és egy öngyújtót kérek",
dstLang: "en",
wantDstText: "cigarettes and a lighter please",
}, // ...
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
got := Translate(d.srcLang, d.dstLang, d.srcText)
if got != d.wantDstText {
t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q",
d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText)
}
})
}
}
Here are a few examples of things to avoid:
// Bad:
// Too wordy.
t.Run("check that there is no mention of scratched records or hovercrafts", ...)
// Slashes cause problems on the command line.
t.Run("AM/PM confusion", ...)
See also Go Tip #117: Subtest Names.
Table-driven tests
Use table-driven tests when many different test cases can be tested using similar testing logic.
- When testing whether the actual output of a function is equal to the
expected output. For example, the many tests of
fmt.Sprintfor the minimal snippet below. - When testing whether the outputs of a function always conform to the same
set of invariants. For example, tests for
net.Dial.
Here is the minimal structure of a table-driven test. If needed, you may use different names or add extra facilities such as subtests or setup and cleanup functions. Always keep useful test failures in mind.
// Good:
func TestCompare(t *testing.T) {
compareTests := []struct {
a, b string
want int
}{
{"", "", 0},
{"a", "", 1},
{"", "a", -1},
{"abc", "abc", 0},
{"ab", "abc", -1},
{"abc", "ab", 1},
{"x", "ab", 1},
{"ab", "x", -1},
{"x", "a", 1},
{"b", "x", -1},
// test runtime·memeq's chunked implementation
{"abcdefgh", "abcdefgh", 0},
{"abcdefghi", "abcdefghi", 0},
{"abcdefghi", "abcdefghj", -1},
}
for _, test := range compareTests {
got := Compare(test.a, test.b)
if got != test.want {
t.Errorf("Compare(%q, %q) = %v, want %v", test.a, test.b, got, test.want)
}
}
}
Note: The failure messages in this example above fulfill the guidance to identify the function and identify the input. There’s no need to identify the row numerically.
When some test cases need to be checked using different logic from other test cases, it is appropriate to write multiple test functions, as explained in GoTip #50: Disjoint Table Tests.
When the additional test cases are simple (e.g., basic error checking) and don’t introduce conditionalized code flow in the table test’s loop body, it’s permissible to include that case in the existing test, though be careful using logic like this. What starts simple today can organically grow into something unmaintainable.
For example:
func TestDivide(t *testing.T) {
tests := []struct {
dividend, divisor int
want int
wantErr bool
}{
{
dividend: 4,
divisor: 2,
want: 2,
},
{
dividend: 10,
divisor: 2,
want: 5,
},
{
dividend: 1,
divisor: 0,
wantErr: true,
},
}
for _, test := range tests {
got, err := Divide(test.dividend, test.divisor)
if (err != nil) != test.wantErr {
t.Errorf("Divide(%d, %d) error = %v, want error presence = %t", test.dividend, test.divisor, err, test.wantErr)
}
// In this example, we're only testing the value result when the tested function didn't fail.
if err != nil {
continue
}
if got != test.want {
t.Errorf("Divide(%d, %d) = %d, want %d", test.dividend, test.divisor, got, test.want)
}
}
}
More complicated logic in your test code, like complex error checking based on conditional differences in test setup (often based on table test input parameters), can be difficult to understand when each entry in a table has specialized logic based on the inputs. If test cases have different logic but identical setup, a sequence of subtests within a single test function might be more readable. A test helper may also be useful for simplifying test setup in order to maintain the readability of a test body.
You can combine table-driven tests with multiple test functions. For example, when testing that a function’s output exactly matches the expected output and that the function returns a non-nil error for an invalid input, then writing two separate table-driven test functions is the best approach: one for normal non-error outputs, and one for error outputs.
Data-driven test cases
Table test rows can sometimes become complicated, with the row values dictating conditional behavior inside the test case. The extra clarity from the duplication between the test cases is necessary for readability.
// Good:
type decodeCase struct {
name string
input string
output string
err error
}
func TestDecode(t *testing.T) {
// setupCodex is slow as it creates a real Codex for the test.
codex := setupCodex(t)
var tests []decodeCase // rows omitted for brevity
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := Decode(test.input, codex)
if got, want := output, test.output; got != want {
t.Errorf("Decode(%q) = %v, want %v", test.input, got, want)
}
if got, want := err, test.err; !cmp.Equal(got, want) {
t.Errorf("Decode(%q) err %q, want %q", test.input, got, want)
}
})
}
}
func TestDecodeWithFake(t *testing.T) {
// A fakeCodex is a fast approximation of a real Codex.
codex := newFakeCodex()
var tests []decodeCase // rows omitted for brevity
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := Decode(test.input, codex)
if got, want := output, test.output; got != want {
t.Errorf("Decode(%q) = %v, want %v", test.input, got, want)
}
if got, want := err, test.err; !cmp.Equal(got, want) {
t.Errorf("Decode(%q) err %q, want %q", test.input, got, want)
}
})
}
}
In the counterexample below, note how hard it is to distinguish between which
type of Codex is used per test case in the case setup. (The highlighted parts
run afoul of the advice from TotT: Data Driven Traps! .)
// Bad:
type decodeCase struct {
name string
input string
codex testCodex
output string
err error
}
type testCodex int
const (
fake testCodex = iota
prod
)
func TestDecode(t *testing.T) {
var tests []decodeCase // rows omitted for brevity
for _, test := tests {
t.Run(test.name, func(t *testing.T) {
var codex Codex
switch test.codex {
case fake:
codex = newFakeCodex()
case prod:
codex = setupCodex(t)
default:
t.Fatalf("Unknown codex type: %v", codex)
}
output, err := Decode(test.input, codex)
if got, want := output, test.output; got != want {
t.Errorf("Decode(%q) = %q, want %q", test.input, got, want)
}
if got, want := err, test.err; !cmp.Equal(got, want) {
t.Errorf("Decode(%q) err %q, want %q", test.input, got, want)
}
})
}
}
Identifying the row
Do not use the index of the test in the test table as a substitute for naming your tests or printing the inputs. Nobody wants to go through your test table and count the entries in order to figure out which test case is failing.
// Bad:
tests := []struct {
input, want string
}{
{"hello", "HELLO"},
{"wORld", "WORLD"},
}
for i, d := range tests {
if strings.ToUpper(d.input) != d.want {
t.Errorf("Failed on case #%d", i)
}
}
Add a test description to your test struct and print it along failure messages. When using subtests, your subtest name should be effective in identifying the row.
Important: Even though t.Run scopes the output and execution, you must
always identify the input. The table test row names must follow the
subtest naming guidance.
Test helpers
A test helper is a function that performs a setup or cleanup task. All failures that occur in test helpers are expected to be failures of the environment (not from the code under test) — for example when a test database cannot be started because there are no more free ports on this machine.
If you pass a *testing.T, call t.Helper to attribute failures in the test
helper to the line where the helper is called. This parameter should come after
a context parameter, if present, and before any remaining
parameters.
// Good:
func TestSomeFunction(t *testing.T) {
golden := readFile(t, "testdata/golden-result.txt")
// ... tests against golden ...
}
// readFile returns the contents of a data file.
// It must only be called from the same goroutine as started the test.
func readFile(t *testing.T, filename string) string {
t.Helper()
contents, err := runfiles.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
return string(contents)
}
Do not use this pattern when it obscures the connection between a test failure
and the conditions that led to it. Specifically, the guidance about
assert libraries still applies, and t.Helper should not be used
to implement such libraries.
Tip: For more on the distinction between test helpers and assertion helpers, see best practices.
Although the above refers to *testing.T, much of the advice stays the same for
benchmark and fuzz helpers.
Test package
Tests in the same package
Tests may be defined in the same package as the code being tested.
To write a test in the same package:
- Place the tests in a
foo_test.gofile - Use
package foofor the test file - Do not explicitly import the package to be tested
# Good:
go_library(
name = "foo",
srcs = ["foo.go"],
deps = [
...
],
)
go_test(
name = "foo_test",
size = "small",
srcs = ["foo_test.go"],
library = ":foo",
deps = [
...
],
)
A test in the same package can access unexported identifiers in the package. This may enable better test coverage and more concise tests. Be aware that any examples declared in the test will not have the package names that a user will need in their code.
Tests in a different package
It is not always appropriate or even possible to define a test in the same
package as the code being tested. In these cases, use a package name with the
_test suffix. This is an exception to the “no underscores” rule to
package names. For example:
-
If an integration test does not have an obvious library that it belongs to
// Good: package gmailintegration_test import "testing" -
If defining the tests in the same package results in circular dependencies
// Good: package fireworks_test import ( "fireworks" "fireworkstestutil" // fireworkstestutil also imports fireworks )
Use package testing
The Go standard library provides the testing package. This is the only
testing framework permitted for Go code in the Google codebase. In particular,
assertion libraries and third-party testing frameworks are not
allowed.
The testing package provides a minimal but complete set of functionality for
writing good tests:
- Top-level tests
- Benchmarks
- Runnable examples
- Subtests
- Logging
- Failures and fatal failures
These are designed to work cohesively with core language features like composite literal and if-with-initializer syntax to enable test authors to write [clear, readable, and maintainable tests].