Go Testing: Difference between revisions
(7 intermediate revisions by the same user not shown) | |||
Line 7: | Line 7: | ||
* [[Go Engineering#Subjects|Go Engineering]] | * [[Go Engineering#Subjects|Go Engineering]] | ||
* [[Go_test_Command#Overview|<code>go test</code> command]] | * [[Go_test_Command#Overview|<code>go test</code> command]] | ||
=TO DO= | |||
<font color=darkkhaki> | |||
* A system to allow testing with real temporary files in <code>./build</code> in Go. | |||
* Integrate see [[Go_Packages#External_Test_Packages|external test packages]]. | |||
* Testing idiom "Introducing Go" page 96. | |||
* Investigate gotest.tools module. | |||
* Running go test immediately after cloning should succeed without any configuration or any running database. If a database is required, those are integration tests. | |||
* https://about.sourcegraph.com/blog/go/advanced-testing-in-go | |||
* Understand unit test lifecycle. Is package initialization performed once per test package? | |||
* Try Testify set up and tear down test suite fixtures, which can save you time and reduce code duplication: https://blog.stackademic.com/using-testify-in-golang-a-comprehensive-guide-8e0417529669 | |||
* Try gotest.tools/v3 v3.0.3. | |||
* A mock framework for Go: https://pkg.go.dev/github.com/golang/mock/gomock | |||
* Testable examples in Go: https://go.dev/blog/examples | |||
* https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/ | |||
</font> | |||
=Overview= | =Overview= | ||
Line 25: | Line 40: | ||
If no tag is declared, the tests will run fine, but we lose the possibility to control when we don't want them to run. They will '''always''' run. | If no tag is declared, the tests will run fine, but we lose the possibility to control when we don't want them to run. They will '''always''' run. | ||
Also see: {{Internal|Software_Testing_Concepts#Unit_Test|Software Testing Concepts | Unit Tests}} | Also see: {{Internal|Software_Testing_Concepts#Unit_Test|Software Testing Concepts | Unit Tests}} | ||
===<tt>testing.T</tt>=== | |||
<code>t.Cleanup(func())</code> | |||
===<span id='Write_a_Unit_Test'></span>Writing a Unit Test=== | ===<span id='Write_a_Unit_Test'></span>Writing a Unit Test=== | ||
Line 98: | Line 117: | ||
ok example.com/a 0.116s | ok example.com/a 0.116s | ||
</font> | </font> | ||
===Table (Tabular) Tests=== | ===Table (Tabular) Tests=== | ||
Line 122: | Line 142: | ||
name string | name string | ||
input string | input string | ||
assert func(require *require.Assertions, result string, err error) | assert func(require *require.Assertions, result string, err error) // can be replaced with 'expectedResult' | ||
// if no error is expected | |||
debug bool // used to stop the debugger on a specific case, by setting a conditional breakpoint on tc.debug | debug bool // used to stop the debugger on a specific case, by setting a conditional breakpoint on tc.debug | ||
}{ | }{ | ||
Line 154: | Line 175: | ||
<syntaxhighlight lang='go'> | <syntaxhighlight lang='go'> | ||
import ( | |||
testifyrequire "github.com/stretchr/testify/require" | |||
"testing" | |||
) | |||
func TestSomeMethod(t *testing.T) { | func TestSomeMethod(t *testing.T) { | ||
testCases := []struct { | testCases := []struct { | ||
name string | name string | ||
input any | input any | ||
require func(require *testifyrequire.Assertions, result any, err error) | |||
}{ | }{ | ||
{}, | {}, | ||
Line 164: | Line 190: | ||
for _, tc := range testCases { | for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | t.Run(tc.name, func(t *testing.T) { | ||
require := testifyrequire.New(t) | |||
result, err := SomeMethod(tc.input) | result, err := SomeMethod(tc.input) | ||
tc. | tc.require(require, result, err) | ||
}) | }) | ||
} | } | ||
Line 219: | Line 246: | ||
{{Internal|Github.com/stretchr/testify#Mocks|Mocks with Testify}} | {{Internal|Github.com/stretchr/testify#Mocks|Mocks with Testify}} | ||
{{Internal|github.com/uber/mock#Overview|Uber gomock}} | {{Internal|github.com/uber/mock#Overview|Uber gomock}} | ||
Latest revision as of 22:07, 3 September 2024
External
Internal
TO DO
- A system to allow testing with real temporary files in
./build
in Go. - Integrate see external test packages.
- Testing idiom "Introducing Go" page 96.
- Investigate gotest.tools module.
- Running go test immediately after cloning should succeed without any configuration or any running database. If a database is required, those are integration tests.
- https://about.sourcegraph.com/blog/go/advanced-testing-in-go
- Understand unit test lifecycle. Is package initialization performed once per test package?
- Try Testify set up and tear down test suite fixtures, which can save you time and reduce code duplication: https://blog.stackademic.com/using-testify-in-golang-a-comprehensive-guide-8e0417529669
- Try gotest.tools/v3 v3.0.3.
- A mock framework for Go: https://pkg.go.dev/github.com/golang/mock/gomock
- Testable examples in Go: https://go.dev/blog/examples
- https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/
Overview
Go comes with a lightweight test framework that includes the go test
command and the testing
package. The tests live in *_test.go
files, located in the package directory.
Packages
Test Types in Go
Unit Tests
somepkg_test.go
files should contain exclusively unit tests, not integration or system tests. Integration tests should be stored in *_integration_test.go
files and system tests in *_system_test.go
files. Optionally, unit test files may include the unit_test
build tag, which allows controlling test execution based on their type (unit, functional and system).
//go:build unit_test
package somepkg
If no tag is declared, the tests will run fine, but we lose the possibility to control when we don't want them to run. They will always run.
Also see:
testing.T
t.Cleanup(func())
Writing a Unit Test
TODO: revisit for Testify.
Write a module as shown here:
For each file containing behavior to test (a.go
)
package a
func Reverse(s string) string {
rs := []rune(s)
var result []rune
for i := len(rs) - 1; i >= 0; i-- {
result = append(result, rs[i])
}
return string(result)
}
add a <file-name>_test.go
test file. In this case a_test.go
. These files are ignored by the compiler and only compiled and executed when go test
is run. These files will be excluded from regular package builds. For more details on how go test
command handles different files, see:
The test files should be part of the same package. If that is the case, the test has access to unexpected identifiers from the package being tested. It is also possible to declare the test files into a corresponding package with the suffix _test
. In this case, the package being tested must be imported explicitly in the test. This is known as "black box" testing. Also see external test packages.
The test files should import testing
.
Add individual tests, as functions starting with TestX..
, where X...
does not start with a lowercase letter, and taking an argument t *testing.T
. The function name serves to identify the test routine. Within these functions, use Error
, Fail
or related methods to signal failure.
func TestX...(t *testing.T) {
...
t.Error("expected this: %q, got that: %q ", ...)
}
package a
import "testing"
func TestReverseEmptyString(t *testing.T) {
expected := ""
result := Reverse("")
if result != expected {
t.Errorf("expected %q, got %q", expected, result)
}
}
func TestReverseOneCharString(t *testing.T) {
expected := "a"
result := Reverse("a")
if result != expected {
t.Errorf("expected %q, got %q", expected, result)
}
}
func TestReverseTwoCharString(t *testing.T) {
expected := "ba"
result := Reverse("ab")
if result != expected {
t.Errorf("expected %q, got %q", expected, result)
}
}
From the module directory, run the tests:
go test
PASS ok example.com/a 0.116s
Table (Tabular) Tests
The "table" ("tabular") format is useful when we need to test a function with a variety of input parameters. The "table" consists in a list of structures (hence the tabular format). Each structure instance provides the test name, the input parameters to be tested, and an assertion function that takes the function result and error and perform assertions correlated to the input. If we simply test for expected results, the assert function can be replaced with the expected results.
Assuming that SomeFunction()
is a function to test:
func SomeFunction(arg string) (string, error) {
return strings.ToUpper(arg), nil
}
then a tabular test would be:
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSomeFunction(t *testing.T) {
testCases := []struct {
name string
input string
assert func(require *require.Assertions, result string, err error) // can be replaced with 'expectedResult'
// if no error is expected
debug bool // used to stop the debugger on a specific case, by setting a conditional breakpoint on tc.debug
}{
{
name: "invocation with empty string",
input: "",
assert: func(require *require.Assertions, result string, err error) {
require.NoError(err)
require.Equal("", result)
},
},
{
name: "invocation with non-empty string",
input: "something",
assert: func(require *require.Assertions, result string, err error) {
require.NoError(err)
require.Equal("SOMETHING", result)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := SomeFunction(tc.input)
tc.assert(require.New(t), result, err)
})
}
}
This is the template:
import (
testifyrequire "github.com/stretchr/testify/require"
"testing"
)
func TestSomeMethod(t *testing.T) {
testCases := []struct {
name string
input any
require func(require *testifyrequire.Assertions, result any, err error)
}{
{},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require := testifyrequire.New(t)
result, err := SomeMethod(tc.input)
tc.require(require, result, err)
})
}
}
Integration Tests
We document implementation of integration tests in go. We use this definition of integration test:
Integration tests, with or without mocks, should be placed in files named *_integration_test.go
files. The name of the file, except the ..._test.go
part that is mandated by the Go test compiler, has no influence over whether the tests contained in it are executed or not. We control the conditional execution with build tags:
//go:build integration_test
package somepkg
. └─ internal └── mypkg ├── mypkg.go ├── mypkg_test.go └── mypkg_integration_test.go
Code Coverage for Integration Tests
TO PROCESS: https://go.dev/blog/integration-test-coverage
Also see:
System Tests
We use this definition of system test:
System tests should be placed in files conventionally named *_system_test.go
files and should declare a system_test
build tag at the top of the file:
//go:build system_test
package somepkg
Benchmarks
Process this: https://pkg.go.dev/testing#hdr-Benchmarks.
Fuzzing
Process this: https://pkg.go.dev/testing#hdr-Fuzzing.
Skipping
Process this: https://pkg.go.dev/testing#hdr-Skipping.
Subtests and Sub-benchmarks
Process this: https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks.
Main
Process this: https://pkg.go.dev/testing#hdr-Main.
Mocks
Mock support in Go: