Golang std testing: Difference between revisions
(→Usage) |
|||
(22 intermediate revisions by the same user not shown) | |||
Line 13: | Line 13: | ||
<blockquote> | <blockquote> | ||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
go test # run | go test # run tests in CWD | ||
go test ./... # run all tests | go test ./... # run all tests | ||
go test -run FooTest # run top-level tests containing 'FooTest' | go test -run FooTest # run top-level tests containing 'FooTest' | ||
go test -run 'FooTest/My TableTest' # run specific table test | |||
go test -run FooTest ./internal/foo # run tests matching 'FooTest in package at '/internal/foo' | 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 print | go test ./internal/foo -v # run internal/foo tests, and print | ||
Line 30: | Line 31: | ||
<blockquote> | <blockquote> | ||
The builtin go test framework is fairly minimalist.<br> | The builtin go test framework is fairly minimalist.<br> | ||
Tests are just functions, you can | Tests are just functions, but you can define subtests with <code>t.Run(...)</code>.<br> | ||
There are no builtin assertions. | |||
Tests are typically kept alongside code. | Tests are typically kept alongside code. | ||
Line 62: | Line 64: | ||
= Sample Strategies = | = Sample Strategies = | ||
<blockquote> | <blockquote> | ||
== | == 1:1 function:test with subtests == | ||
<blockquote> | <blockquote> | ||
The [https://github.com/nanomsg/mangos/tree/3c90520e87f5718a99fc40991345fc4760136dc1 mangos] library uses a really clean approach.<br> | The [https://github.com/nanomsg/mangos/tree/3c90520e87f5718a99fc40991345fc4760136dc1 mangos] library uses a really clean approach.<br> | ||
Line 68: | Line 70: | ||
* Instantiates a struct | * Instantiates a struct | ||
* Asserts on all public methods of the struct | * Asserts on all public methods of the struct using <code>t.Run(...)</code> | ||
{| class="wikitable" | {| class="wikitable" | ||
Line 77: | Line 79: | ||
|- | |- | ||
|} | |} | ||
</blockquote><!-- | </blockquote><!-- 1:1 function:test with subtests --> | ||
== Table Tests == | == Table Tests == | ||
Line 89: | Line 91: | ||
expects string | expects string | ||
}{ | }{ | ||
{ test: " | { test: "Valid Name", name: "Adam", }, | ||
{ test: " | { test: "Nil Name", name: nil, }, | ||
{ test: " | { test: "Invalid Name", name: nil, }, | ||
} | } | ||
for _, tcase := range tcases { | for _, tcase := range tcases { | ||
t.Run(tcase.test, func(t *testing.T) { // <-- t.Run() evaluates each case | t.Run(tcase.test, func(t *testing.T) { // <-- t.Run() evaluates each case | ||
res := hello.Hello(tcase.name) | if res := hello.Hello(tcase.name); res != tcase.expects { | ||
t.Errorf("Failed because...") | t.Errorf("Failed because...") | ||
} | } | ||
Line 105: | Line 106: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
</blockquote><!-- Table Tests --> | </blockquote><!-- Table Tests --> | ||
== Override Function == | |||
<blockquote> | |||
Go allows you to assign a function to a global variable.<br> | |||
You abstract a real method call with this substitute,<br> | |||
and override the substitute within a test. | |||
Note that this only works if a function is saved to a var | |||
* You cannot override regular functions in tests (ex. <code>func Foo() {}</code>) | |||
* You cannot override methods in tests (ex. <code>func (this *Foo) Bar() {}</code>) | |||
<syntaxhighlight lang="go"> | |||
// foo.go | |||
var removeFile = os.Remove | |||
func DoThing(path string) { | |||
removeFile(path) | |||
} | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="go"> | |||
// 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...") | |||
} | |||
} | |||
</syntaxhighlight> | |||
</blockquote><!-- --> | |||
== Interface/Substitute Concretion == | |||
<blockquote> | |||
If you're covering a large set of methods throughout your application,<br> | |||
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,<br> | |||
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). | |||
{{ expand | |||
| production | |||
| | |||
<syntaxhighlight lang="go"> | |||
// internal/filesystem/interface.go | |||
package filesystem | |||
type Interface interface { | |||
Remove(name string) error | |||
} | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="go"> | |||
// 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) | |||
} | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="go"> | |||
// internal/filesystem/fs.go | |||
package filesystem | |||
var Fs = Raw{} // exported variable, that we can override for entire application within tests | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="go"> | |||
// foo.go | |||
package foo | |||
import "example.com/x/internal/filesystem" | |||
func Foo() error { | |||
return filesystem.Fs.Remove("/var/tmp/foo.txt") | |||
} | |||
</syntaxhighlight> | |||
}} | |||
{{ expand | |||
| testing | |||
| | |||
<syntaxhighlight lang="go"> | |||
// 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 | |||
} | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="go"> | |||
// 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) | |||
}) | |||
} | |||
</syntaxhighlight> | |||
}} | |||
</blockquote><!-- Interface/Substitute Package --> | |||
</blockquote><!-- Sample Strategies --> | </blockquote><!-- Sample Strategies --> | ||
= Assertions = | = Components = | ||
<blockquote> | |||
== Assertions == | |||
<blockquote> | <blockquote> | ||
There are no assertions, you are responsible for tests and messages. | There are no assertions, you are responsible for tests and messages. | ||
Line 125: | Line 255: | ||
</blockquote><!-- Assertions --> | </blockquote><!-- Assertions --> | ||
= Subtests = | == Subtests == | ||
<blockquote> | <blockquote> | ||
Evaluate subtests under one function using <code>t.Run("TESTNAME", ...)</code>. | Evaluate subtests under one function using <code>t.Run("TESTNAME", ...)</code>. | ||
Line 174: | Line 304: | ||
</blockquote><!-- Subtests --> | </blockquote><!-- Subtests --> | ||
= Benchmarking = | == Setup/Teardown == | ||
<blockquote> | |||
There is no framework-set way of doing this, just run a function. | |||
<syntaxhighlight lang="go"> | |||
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(t) | |||
// ... | |||
} | |||
} | |||
</syntaxhighlight> | |||
</blockquote><!-- Setup/Teardown --> | |||
== Benchmarking == | |||
<blockquote> | <blockquote> | ||
There are tools for benchmarking. See docs | There are tools for benchmarking. See docs | ||
</blockquote><!-- Benchmarking --> | </blockquote><!-- Benchmarking --> | ||
= Fuzzing = | == Logging == | ||
<blockquote> | |||
Tests have a logger, that you can use to print information during test runs.<br> | |||
(only shown on failing tests) | |||
<syntaxhighlight lang="go"> | |||
func TestThing(t *testing.T) { | |||
t.Log("Show this while testing") | |||
} | |||
</syntaxhighlight> | |||
</blockquote><!-- Logging --> | |||
== Fuzzing == | |||
<blockquote> | <blockquote> | ||
There are tools for fuzzing tests. See docs | There are tools for fuzzing tests. See docs | ||
</blockquote><!-- Fuzzing --> | </blockquote><!-- Fuzzing --> | ||
</blockquote><!-- Components --> |
Latest revision as of 22:05, 30 July 2022
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/My TableTest' # run specific table test 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.Note that this only works if a function is saved to a var
- You cannot override regular functions in tests (ex.
func Foo() {}
)- You cannot override methods in tests (ex.
func (this *Foo) Bar() {}
)// 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(t) // ... } }Benchmarking
There are tools for benchmarking. See docs
Logging
Tests have a logger, that you can use to print information during test runs.
(only shown on failing tests)func TestThing(t *testing.T) { t.Log("Show this while testing") }Fuzzing
There are tools for fuzzing tests. See docs