Golang std testing
Go ships with a minimalist test suite.
Documentation
testing
https://pkg.go.dev/testing@go1.18.3
Usage
go test # run tests in CWD go test ./... # run all tests go test -run FooTest # run top-level tests containing 'FooTest' go test -run FooTest ./internal/foo # run tests matching 'FooTest in package at '/internal/foo' go test ./internal/foo -v # run internal/foo tests, and printyou can also create an ad-hoc test-package and run exclusively a ad-hoc test.go package,
but this is not considered the normgo test internal/foo/bar_test.go
Example
The builtin go test framework is fairly minimalist.
Tests are just functions, but you can define subtests witht.Run(...)
.
There are no builtin assertions.Tests are typically kept alongside code.
// myproject/mypackage/mylib.go package mypackage func Hello(name string) string { return "Hello, " + name }// myproject/mypackage/mylib_test.go package mypackage import "testing" func TestHello(t *testing.T) { res := Hello("Adam") if res != "Hello, Adam" { t.Errorf("Hello() result did not match") } }
Sample Strategies
1:1 function:test with subtests
The mangos library uses a really clean approach.
A library of assertions is created, then each test
- Instantiates a struct
- Asserts on all public methods of the struct using
t.Run(...)
https://github.com/nanomsg/mangos/blob/master/test/certs_test.go test cases (per struct) https://github.com/nanomsg/mangos/blob/master/test/util.go test assertions Table Tests
See subtests below.
func TestHello(t *testing.T) { tcases := []struct { // <-- slice of structs containing testdata test string name string expects string }{ { test: "Valid Name", name: "Adam", }, { test: "Nil Name", name: nil, }, { test: "Invalid Name", name: nil, }, } for _, tcase := range tcases { t.Run(tcase.test, func(t *testing.T) { // <-- t.Run() evaluates each case if res := hello.Hello(tcase.name); res != tcase.expects { t.Errorf("Failed because...") } } } }Override Function
Go allows you to assign a function to a global variable.
You abstract a real method call with this substitute,
and override the substitute within a test.// foo.go var removeFile = os.Remove func DoThing(path string) { removeFile(path) }// foo_test.go func TestDoThing(t *testing.T) { var removedPath string removeFile = func(path string) { removedPath = path } DoThing("/var/abc.txt") if removedPath != "/var/abc.txt" { t.Errorf("Failed because...") } }Interface/Substitute Concretion
If you're covering a large set of methods throughout your application,
you probably want to abstract it with an interface.You can define a package with an exported variable with a concretion of the interface in production,
within tests, you can override the variable with a testable stub object.Filesystem operations are a good example of this (but consider using golang afero or similar).
production
// internal/filesystem/interface.go package filesystem type Interface interface { Remove(name string) error }// internal/filesystem/raw.go package filesystem import "os" type Raw struct{} // implements 'example.com/x/internal/filesystem.Filesystem' func (this *Raw) Remove(name string) error { return os.Remove(name) }// internal/filesystem/fs.go package filesystem var Fs = Raw{} // exported variable, that we can override for entire application within tests// foo.go package foo import "example.com/x/internal/filesystem" func Foo() error { return filesystem.Fs.Remove("/var/tmp/foo.txt") }
testing
// internal/filesystem/stub.go package filesystem type Stub struct { // implements 'example.com/x/internal/filesystem.Filesystem' RemoveError error } func (this *Raw) Remove(name string) error { return this.RemoveError }// foo_test.go package foo import "example.com/x/internal/filesystem" func TestMain(t *testing.T) { // swap out fs with our stub var ExpectedError = errors.New("Expected") filesystem.Fs = filesystem.Stub{RemoveError: ExpectedError} func t.Run("Raises Error", func(t *testing.T) { err := foo.Foo() assert.Error(t, err, ExpectedError) }) }
Components
Assertions
There are no assertions, you are responsible for tests and messages.
func TestHello(t *testing.T) { // log message and fail (but continue executing) t.Errorf("expected: %v\nreceived: %v", expects, received) t.Fail() // mark test as failed, but continue t.FailNow() // mark test as failed and stop executing t.Skip("Reason") // log, and stop executing t.TempDir() // provides a tempdir that is deleted once test finishes running }Subtests
Evaluate subtests under one function using
t.Run("TESTNAME", ...)
.These can be expressed as Table Tests:
import "example.com/x/hello" // <-- despite sharing package, must import package in test func TestHello(t *testing.T) { tcases := []struct { // <-- slice of structs containing testdata test string name string expects string }{ { test: "ValidName", name: "Adam", }, { test: "NilName", name: nil, } } for _, tcase := range tcases { t.Run(tcase.test, func(t *testing.T) { // <-- t.Run() evaluates each case res := hello.Hello(tcase.name) if res != tcase.expects { t.Errorf("Failed because...") } } } }Or you can define simply unique subtests
import "github.com/stretchr/testify/assert" import "regexp" import "testing" func TestHello(t *testing.T) { t.Run("Begins with H", func(t *testing.T) { res := hello.Hello("Alex") assert.Regexp(t, regexp.MustCompile("^h"), res) } t.Run("Last word is name", func(t *testing.T) { res := hello.Hello("Alex") assert.Regexp(t, regexp.MustCompile("Alex$"), res) }) }You can also implement setup/teardown this way using
defer
functions.Setup/Teardown
There is no framework-set way of doing this, just run a function.
func setup(t *testing.T) { t.Log("setup") } func teardown(t *testing.T) { t.Log("teardown") } func TestHello(t *testing.T) { t.Run("Tests that ..", func(t *testing.T) { t.setup(t) defer t.teardown() // ... } }Benchmarking
There are tools for benchmarking. See docs
Logging
Tests have a logger, that you can use to print information during test runs
func TestThing(t *testing.T) { t.Log("Show this while testing") }Fuzzing
There are tools for fuzzing tests. See docs