跳到正文
awesomego.org

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.Sprintf or 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.go file
  • Use package foo for 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].