File-driven testing in Go - Eli Bendersky's website
Eli Bendersky's websiteThis is a quick post about a testing pattern I've found useful in multipleprogramming languages; Go's testing package makes it particularly easyto use and implement.
Table-driven tests are a well-known and recommended technique for structuringunit tests in Go. I assume the reader is familiar with table-driven tests; ifnot, check out this basic example or google for the manytutorials available online.
In some cases, the input to a test can be more data than we're willingto paste into a _test.go file, or the input can be non-text. For thesecases, file-driven tests are a useful extension to the table-driven technique.
Suppose - for the sake of demonstration - that we want to test Go'sgo/format package. This package provides programmatic access to gofmt'scapabilities with the Source functionthat takes unformatted Go text as input and produces formatted text. We coulduse table-driven tests for this, but some inputs may be large - whole Go files,or significant chunks thereof. It can be more convenient to have these inputs inexternal files the test can read and invoke format.Source on.
This toy GitHub repository shows howit's done. Here is the directory structure:
$ tree.├── go.mod└── somepackage ├── somepackage_test.go └── testdata ├── funcs.golden ├── funcs.input ├── simple-expr.golden └── simple-expr.input
Our test code is in somepackage_test.go - we'll get to it shortly. Alongsideit is the testdata directory with pairs of files named <name>.inputand <name>.golden. Each pair serves as a test: the input file is fed intothe formatter, and the formatter's output is compared to the golden file.
This is the entirety of our test code:
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) } }) }}The code is well commented, but here are a few highlights:
- The test file pairs are auto-discovered using filepath.Glob. Placingadditional file pairs in the testdata directory will automatically ensurethey are used by subsequent test executions. When we run go test, thecurrent working directory will be set to the package that's being tested, sofinding the testdata directory is easy.
- The testdata name is special for the Go toolchain, which willignore files in it (so you can place files named *.go there,for example, and they won't be built or analyzed).
- For each file pair we create a subtestwith T.Run. This means it's a separatetest as far as the test runner is concerned - reported on its own in verboseoutput, can be run in parallel with other tests, etc.
If we run the tests, we get:
$ 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)PASSok example.com/somepackage 0.002s
Note how each input file in testdata generated its own test with a distinctname.
The "golden file" approach shown here is just one of the possible patterns forusing file-driven tests. Often there is no separate file for the expectedoutput, and instead the input file itself contains some special markers thatdrive the test's expectations. It's really dependent on the specific testingscenario; the Go project and subprojects (such as x/tools) use severalvariations of this testing pattern.
本文章由 flowerss 抓取自RSS,版权归源站点所有。