Go - Testing
Test Files
- Test files have file names ending in
_test.go
- Test files may contain: tests, benchmarks, examples and fuzz tests
- The
*_test.go
files are excluded from regular package builds
Test Package
A test file can be in the same package as the one being tested.
// A test file in the same package may refer to unexported identifiers.package calculator
It’s a good practice for the test file to be in a corresponding package with the suffix _test
.
// A test file in a separate `_test` package, may only use the exported identifiers.package calculator_test// The package being tested must be importedimport ( "calculator")
Test Functions
Test function signature:
// Starts with the word `Test`.// Where `Xxx` does not start with a lowercase letter.func TestXxx(t *testing.T)
Example:
package calculator_testimport ( "calculator" // The package being tested "testing" // Standard library testing package)func TestAdd(t *testing.T) { // By convention the expected result is defined as a `want` variable // and the actual result as a `got` variable. want := 17 got := calculator.Add(10, 7) // Compare the expected result with the actual result. if want != got { // Make the test fail with a formatted error message showing the // expected and actual values. t.Errorf("want %d, got %d", want, got) }}
Test Failure
Tests pass by default unless we explicitly make them fail with the Error
or Fatal
methods.
func TestSomething(t *testing.T) { // Report an error and continue execution of the test. t.Error("expected an error, got nil") t.Errorf("want %d, got %d", want, got) // Formatted error message // Report an error and abort the test. // Execution will continue at the next test or benchmark. t.Fatal(err) t.Fatalf("did not expect an error, got %v", err) // Formatted error message}
Logging Text
Text can be logged with the Log
and Logf
methods.
func TestSomething(t *testing.T) { // Record the text in the error log (newline is added if not provided). // The text will be printed if the test fails or the verbose flag `-v` is set. // For benchmarks, the text is always printed. t.Log("report a useful message") t.Logf("another useful message with data: %v", data) // Formatted error message}
Run Tests
The tests are run in 2 different modes:
1. Local Directory Mode
When go test
is invoked with no package arguments, it runs the tests in the current directory.
$go test
2. Package List Mode
When go test
is invoked with explicit package arguments, it runs tests for the listed packages.
$# Run the tests for a specific package.$go test packageName$go test moduleName/subPackageName$# Run the tests for multiple packages.$go test package1 package2 package3$# Run the tests for the package in the current directory.$go test .$# Run the tests for the current package and all the sub-packages.$go test ./...
In the package list mode, successful test results are cached.
To disable test caching:
$# Disable test caching with `-count 1`.$go test -count 1 ./...
Run Specific Tests
$# The -run flag is followed by a regexp to match the functions to run.$go test -run NameToMatch$# Match the functions to run in the current package and all sub-packages.$go test -run NameToMatch ./...
Verbose Test Output
$# Run the tests with verbose output.$go test -v$# Log verbose output and test results in JSON.$go test -json
Test Timeout
Run the tests with a timeout (Default 10m
):
$go test -timeout 10s
Run Tests in Parallel
The Parallel
method can be used to run the test in parallel with other parallel tests.
func TestFoo(t *testing.T) { t.Parallel()}
By default the number of tests run in parallel is equal to GOMAXPROCS
(number of CPUs), the -parallel
flag can be used to define this number explicitly.
$go test -parallel 16
Comparisons
The equality operators ==
and !=
can be used to compare basic types like integers, floats, strings and booleans.
func TestSomething(t *testing.T) { if want != got { t.Errorf("want %v, got %v", want, got) }}
Errors can be comapred against nil
.
func TestSomething(t *testing.T) { if err != nil { t.Errorf("expected no error, got %v", err) } if err == nil { t.Error("expected an error, got nil") }}
Specific errors can be checked using errors.Is
and errors.As
.
import ( "testing" "errors")func TestSomething(t *testing.T) { // Check the error type. if errors.Is(err, ErrSomeError) { t.Error("got error of type ErrSomeError") } // If the error value is needed for the test. var errVal ErrSomeError if errors.As(err, &ErrSomeError) { t.Errorf("got error code %d", errVal.Code) }}
The reflect.DeepEqual
function can be used to compare slices, maps, and structs.
import ( "testing" "reflect")func TestSomething(t *testing.T) { if !reflect.DeepEqual(want, got) { t.Errorf("want %v, got %v", want, got) }}
The cmp
package is a better alternative to reflect.DeepEqual
for comparing values and displaying the difference.
import ( "testing" "github.com/google/go-cmp/cmp")func TestSomething(t *testing.T) { if cmp.Equal(want, got) { t.Error(cmp.Diff(want, got)) // Display the difference }}
Unlike reflect.DeepEqual
, unexported struct fields are not compared by default and result in a panic unless they are ignored or explicitly compared.
import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts")// Struct with unexported fields.type Example struct { a, b, c int Foo, Bar string}func TestSomething(t *testing.T) { // Ignore all unexported struct fields. if !cmp.Equal(want, got, cmpopts.IgnoreUnexported(Example{})) { t.Error(cmp.Diff(want, got)) } // Forcibly introspect unexported fields of the specified structs. if !cmp.Equal(want, got, cmp.AllowUnexported(Example{})) { t.Error(cmp.Diff(want, got)) }}
Subtests
The Run
method allows defining subtests without having to define separate functions.
func TestSomething(t *testing.T) { // The first argument is the name of the subtest. // `Run` runs the function in a separate goroutine and blocks until it returns. t.Run("Subtest 1", func(t *testing.T) { // ... }) t.Run("Subtest 2", func(t *testing.T) {})}
Each subtest has a unique name, a combination of the top level test name and the name passed to Run
separated by a slash.
The name can be used to run individual subtests using the -run
flag:
$go test -run TestSomething/Subtest_1$go test -run TestSomething/Subtest_2
Subtests provide a way to share common setup and tear down code.
func TestSomething(t *testing.T) { // Setup code v := Setup() t.Run("Subtest 1", func(t *testing.T) {}) t.Run("Subtest 2", func(t *testing.T) {}) t.Run("Subtest 3", func(t *testing.T) {}) // Teardown code TearDown(v)}
Parallel Subtests
Subtests can be used to run a group of tests in parallel with each other but not with other parallel tests.
// The outer test will not complete until all the parallel tests have completed.func TestParallelSubtests(t *testing.T) { t.Run("Subtest 1", func(t *testing.T) { t.Prallel() }) t.Run("Subtest 2", func(t *testing.T) { t.Prallel() }) t.Run("Subtest 3", func(t *testing.T) { t.Prallel() })}
Parallel tests that share common resources can be grouped together to wait for them to complete before cleaning up the shared resources.
func TestParallelGroup(t *testing.T) { // Setup code v := Setup() // This subtest will not return until its parallel subtests complete. t.Run("Group", func(t *testing.T) { t.Run("Test 1", func(t *testing.T) { t.Prallel() }) t.Run("Test 2", func(t *testing.T) { t.Prallel() }) t.Run("Test 3", func(t *testing.T) { t.Prallel() }) }) // Teardown code TearDown(v)}
Table Driven Tests
A series of related checks can be implemented by looping over a slice of test cases:
func TestAdd(t *testing.T) { // A struct that defines the test inputs and expected output. type testCase struct { a, b int // The test inputs want int // The expected output } // The test cases to run (slice of testCase). testCases := []testCase{ {a: 10, b: 7, want: 17}, {a: -10, b: 20, want: 10}, {a: -1, b: 0, want: -1}, {a: -1, b: 100, want: 99}, // Adding new tests later is easier. } for _, tc := range testCases { got := calculator.Add(tc.a, tc.b) if tc.want != got { // Display the function name and inputs in the failure message. t.Errorf("Add(%d, %d): want %d, got %d", tc.a, tc.b, tc.want, got) } }}
Using an anonymous struct literal:
func TestAdd(t *testing.T) { // An anonymous struct literal can be used instead of creating a named type. testCases := []struct { a, b int want int }{ {a: 10, b: 7, want: 17}, {a: -10, b: 20, want: 10}, {a: -1, b: 0, want: -1}, {a: -1, b: 100, want: 99}, } // ...}
Naming test cases:
func TestSplit(t *testing.T) { // Using a map to name the test cases and display the name on failure. // Maps are unordered, the tests are going to run in a different order each time. testCases := map[string]struct { input string sep string want []string }{ "space": {input: "a b c", sep: " ", want: []string{"a", "b", "c"}}, "comma": {input: "a,b,c", sep: ",", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a,b,c", sep: "/", want: []string{"a,b,c"}}, } // Loop over the test cases and run each one as a subtest. for name, tc := range testCases { // Use the name as the subtest name. t.Run(name, func(t *testing.T) { got := strings.Split(tc.input, tc.sep) if !cmp.Equal(tc.want, got) { t.Error(cmp.Diff(tc.want, got)) // Display the diff on failure } }) }}
Running individual subtests:
$go test -run TestSplit/space$go test -run TestSplit/comma$go test -run TestSplit/wrong_sep
Test Helpers
The Helper
method marks the calling function as a test helper function.
Failures will be reported at the calling test function and not in the helper function which will be skipped.
// Example helper function.func assertStringEqual(t testing.TB, want, got string) { t.Helper() // Mark this function as a test helper if want != got { t.Errorf("want %q, got %q", want, got) }}// Test function using the helper function.func TestSomething(t *testing.T) { // ... assertStringEqual(t, want, got) // Failure will be reported at this line}
The testing.TB
is the interface common to testing.T
, testing.B
, and testing.F
, it makes the helper function usable from test, benchmark and fuzz test functions.
Cleanup
The Cleanup
method registers a function to be called when the test and all its subtests complete.
Cleanup functions are called in LIFO order (Last in first out).
func TestSomething(t *testing.T) { resource := createResource() t.Cleanup(func() { // Cleanup after the test. resource.Close() })}
Cleanup functions can be used in helper functions.
func createTestResource(t *testing.T) *Resource { resource := createResource() // The cleanup will happen after the test has completed. // If the `defer` statement was used, the function will be called when // the helper function returns and not when the test completes. t.Cleanup(func() { resource.Close() }) return resource}
Temporary Directory
The TempDir
method can be used to create a temporary directory that will be automatically removed on cleanup when the test and all its subtests complete.
func TestSomething(t *testing.T) { temp1 := t.TempDir() temp2 := t.TempDir() // Each call returns a unique directory}
Test Data
A directory named testdata
can be used to hold data needed by the tests.
The go
tool will ignore directories named testdata
.
func TestSomething(t *testing.T) { // Read a file used by the test. f, err := os.ReadFile("testdata/data.json") // ...}
Skipping
$# The -run flag is followed by a regexp to match the functions to skip.$go test -skip NameToMatch
Tests or benchmarks may be skipped at run time by calling the Skip()
method:
func TestTimeConsuming(t *testing.T) { // `testing.Short()` is `true` when the tests are run in short mode. if testing.Short() { t.Skip("skipping test in short mode.") }}
Run the tests in short mode:
$go test -short
Race Detector
Run the tests with the race detector to detect race conditions:
$$ go test -race
List Functions
List all the tests, benchmarks, fuzz tests and examples without running them:
$# The -list flag is followed by a regexp to match the functions to list.$go test -list .
Fuzzing
Fuzz test function signature:
// Starts with the word `Fuzz`.// Where `Xxx` does not start with a lowercase letter.func FuzzXxx(f *testing.F)
Example:
func FuzzFoo(f *testing.F) { // Add the seed inputs to the seed corpus for the fuzz test. // Seed inputs are optional but they can be used to guide the fuzzing engine. // The arguments must match the arguments for the fuzz target. // The seed inputs are run by default even when not fuzzing. f.Add(5, "hello") f.Add(100, "world") // Can be called as many times as we want. // The function passed to `f.Fuzz` is the fuzz target. // It takes a `*testing.T` parameter followed by one or more // parameters for random inputs. f.Fuzz(func(t *testing.T, i int, s string) { out, err := Foo(i, s) // When fuzzing, we can't predict the expected output, since we don't have // control over the inputs, instead we can verify the properties of the output. if err != nil && out != "" { t.Errorf("%q, %v", out, err) } })}
Run the fuzz test:
$# Without the -fuzz flag, the fuzz test will run with the seed inputs only.$go test$# Run the fuzz test with randomly generated inputs.$# The -fuzz flag is followed by a regexp to match the function to run.$# Fuzzing will not run if the regexp matches more than one fuzz test.$go test -fuzz .$# Match a single fuzz test function to run.$# The fuzz test will never terminate if no failures were found.$# The test can be terminated with Ctrl-C.$go test -fuzz FuzzSomething$# Run the fuzz target for a specified duration (Default: forever).$go test -fuzz . -fuzztime 1h30m$# Run the fuzz target N times.$go test -fuzz . -fuzztime 1000x
When fuzzing is enabled, the fuzz target is called with arguments generated by repeatedly making random changes to the seed inputs.
When the fuzz target fails for a given input, the inputs that caused the failure are written to a seed corpus file in the testdata/fuzz/<Name>
directory within the package.
When fuzzing is disabled, the fuzz target is called with the seed inputs added with the Add
method and seed inputs from the testdata/fuzz/<Name>
directory.
$# The entries saved to the `testdata/fuzz` directory are run by default$# whether fuzzing or not.$# In this mode the fuzz test acts like a regular test.$go test
Benchmarks
Benchmark function signature:
// Starts with the word `Benchmark`.// Where `Xxx` does not start with a lowercase letter.func BenchmarkXxx(b *testing.B)
The benchmark function must run the target code b.N
times:
func BenchmarkRepeat(b *testing.B) { // The benchmark code is executed `b.N` times. // The time taken to run the code is also measured. for i := 0; i < b.N; i++ { SomeFunction() // Code to benchmark }}
The timer can be reset if the benchmark needs some expensive setup before running:
func BenchmarkExpensiveSetup(b *testing.B) { // Expensive setup code that might take time // that should not be included in the benchmark time. a := ExpensiveSetup() b.ResetTimer() // Reset the timer after an expensive setup for i := 0; i < b.N; i++ { a.DoSomething() }}
By default, no benchmark functions are run with go test
.
Run the benchmarks with the -bench
flag:
$# The -bench flag is followed by a regexp to match the functions to run.$# In this case run all the benchmarks.$go test -bench .$# Run a specific benchmark function.$go test -bench BenchmarkFunctionName$# Run iterations of each benchmark to take a specific time.$go test -bench . -benchtime 10m30s # Default 1s$# Run the benchmark N times.$go test -bench . -benchtime 1000x
Examples
Example functions are used to demonstrate the usage of a package’s components.
Instead of reporting success or failure, example functions print the output to os.Stdout
.
Godoc displays the examples in the documentation of the package.
// Example function that is compiled but not executed.func ExampleXxx() { // ...}// If the last comment in the function starts with `Output:` then// the function is executed and the output is checked against the comment.// The output comparison ignores leading and trailing spaces.func ExampleXxx() { // Output: expected output}// An example with no text after `Output:` is executed and expected// to produce no output.func ExampleXxx() { // Output:}// To comment `Unordered output:` can be used to ignore the order of the lines.func ExampleXxx() { // Unordered output: // 3 // 1 // 2}
Examples:
// This example function is compiled but not executed (No output comment).func ExamplePrintln() { fmt.Println("Hello")}// This example function is executed and its output will be checked.func ExampleAdd() { sum := calculator.Add(1, 5) fmt.Println(sum) // Output: 6}// This example function is executed and its output will be checked but the// lines order is ignored.func ExampleMap() { // Maps are not ordered m := map[int]string{1: "a", 2: "b", 3: "c"} for k, v := range m { fmt.Println(k, v) } // Unordered output: // 3 c // 2 b // 1 a}
Naming conventions:
// Example for the package.func Example() { ... }// Example for a function F.func ExampleF() { ... }// Example for a type T.func ExampleT() { ... }// Example for a method M on type T.func ExampleT_M() { ... }// Multiple example functions may be provided by appending a suffix (lowercase).func Example_suffix() { ... }func ExampleF_suffix() { ... }func ExampleT_suffix() { ... }func ExampleT_M_suffix() { ... }
Test Coverage
Coverage analysis can be enabled with the -cover
flag:
$# Run the tests and print the coverage results.$go test -cover
Detailed coverage information:
$# Save the coverage results to a file.$go test -coverprofile=coverage.out$# Print detailed results.$go tool cover -func=coverage.out$# Or view the coverage results as an HTML page.$go tool cover -html=coverage.out