File-driven tests extend the familiar table-driven approach to cases where the input is too bulky to embed in a _test.go file, or simply isn't text. Instead of literal values in a slice, each test case lives in its own file on disk.

A worked example is testing the go/format package, whose Source function takes unformatted Go text and returns the formatted version. Whole files or large fragments are awkward as table entries but natural as external inputs.

Sample project available here:

$ tree
.
├── go.mod
└── somepackage
    ├── somepackage_test.go
    └── testdata
        ├── funcs.golden
        ├── funcs.input
        ├── simple-expr.golden
        └── simple-expr.input

The testdata directory sits next to somepackage_test.go and holds file pairs named <name>.input and <name>.golden. Each pair constitutes one test: the input goes through the formatter, and the result is checked against the golden output.

func TestFormatFiles(t *testing.T) {
  // Find the paths of all input files in the data directory.
  paths, err := filepath.Glob(filepath.Join("testdata", "*.input"))
  if err != nil {
    t.Fatal(err)
  }

  for _, path := range paths {
    _, filename := filepath.Split(path)
    testname := filename[:len(filename)-len(filepath.Ext(path))]

    // Each path turns into a test: the test name is the filename without the
    // extension.
    t.Run(testname, func(t *testing.T) {
      source, err := os.ReadFile(path)
      if err != nil {
        t.Fatal("error reading source file:", err)
      }

      // >>> This is the actual code under test.
      output, err := format.Source(source)
      if err != nil {
        t.Fatal("error formatting:", err)
      }
      // <<<

      // Each input file is expected to have a "golden output" file, with the
      // same path except the .input extension is replaced by .golden
      goldenfile := filepath.Join("testdata", testname+".golden")
      want, err := os.ReadFile(goldenfile)
      if err != nil {
        t.Fatal("error reading golden file:", err)
      }

      if !bytes.Equal(output, want) {
        t.Errorf("\n==== got:\n%s\n==== want:\n%s\n", output, want)
      }
    })
  }
}

Mechanics worth noting

  • Pairs are auto-discovered via filepath.Glob, so dropping new files into testdata is enough to register new tests. Because go test sets the working directory to the package under test, locating testdata is trivial.
  • The Go toolchain treats testdata specially and skips its contents — files named *.go can live there without being built or analyzed.
  • Each pair runs as a subtest created with T.Run, so the test runner reports it separately in verbose output and it can run in parallel with siblings.

The result of a test run:

$ go test -v ./...
=== RUN   TestFormatFiles
=== RUN   TestFormatFiles/funcs
=== RUN   TestFormatFiles/simple-expr
--- PASS: TestFormatFiles (0.00s)
    --- PASS: TestFormatFiles/funcs (0.00s)
    --- PASS: TestFormatFiles/simple-expr (0.00s)
PASS
ok    example.com/somepackage 0.002s

Every input file surfaces as its own named test.

Variations on the pattern

Golden files are only one option. Frequently the expected output has no separate file at all: the input carries special markers that the test interprets. Which shape fits depends on what's being tested, and the Go project and its subprojects such as x/tools employ several variants of this technique.