testgroup
helps you organize tests into groups. A test group is a struct
(or
other type) whose exported methods are its subtests. The subtests can share
data, helper functions, and pre/post-group and pre/post-test hooks.
testgroup
was inspired by
github.com/stretchr/testify/suite
.
- Support for parallel execution of tests, including waiting for parallel tests to finish
- Easy access to assertion helpers
- Pre/post-group and pre/post-test hooks
Here's a simple test group from
example_absint_test.go
:
package testgroup_test
import (
"testing"
"github.com/bloomberg/go-testgroup"
)
func AbsInt(n int) int {
if n < 0 {
return -n
}
return n
}
// This function is the entry point to the test group.
// Because it starts with "Test" and accepts a *testing.T argument,
// it is detected and called by the Go testing package when you run "go test".
func TestAbsInt(t *testing.T) {
testgroup.RunSerially(t, &AbsIntTests{})
}
type AbsIntTests struct {
// Group-wide data/state can be stored here.
}
func (grp *AbsIntTests) DoesNotChangeNonNegativeNumbers(t *testgroup.T) {
t.Equal(0, AbsInt(0))
t.Equal(1, AbsInt(1))
t.Equal(123456789, AbsInt(123456789))
}
func (grp *AbsIntTests) MakesNegativeNumbersPositive(t *testgroup.T) {
t.Equal(1, AbsInt(-1))
t.Equal(123456789, AbsInt(-123456789))
}
When you run go test
, you'll see something like this:
$ go test -v example_absint_test.go
=== RUN TestAbsInt
=== RUN TestAbsInt/DoesNotChangeNonNegativeNumbers
=== RUN TestAbsInt/MakesNegativeNumbersPositive
--- PASS: TestAbsInt (0.00s)
--- PASS: TestAbsInt/DoesNotChangeNonNegativeNumbers (0.00s)
--- PASS: TestAbsInt/MakesNegativeNumbersPositive (0.00s)
PASS
ok command-line-arguments 0.014s
testgroup
was inspired by testify's suite package.
We really like testify/suite
, but we had trouble getting subtests to run in
parallel. Testify stores each test's testing.T
inside the Suite
struct,
which means that only one testing.T
is available at a given time. If you run
tests in parallel, failures can be reported as another test failing, and if a
test failure is reported twice, testing
panics.
After separating the group-wide state from the test-specific state, we also made
a few usability improvements to the testing.T
-like object.
A test group is a struct
(or other type) whose exported methods are its
subtests.
type MyGroup struct{}
func (*MyGroup) Subtest(t *testgroup.T) {
// ...
}
testgroup
considers all of the type's exported methods to be subtests.
(Unlike testing
-style tests or testify/suite
subtests, you don't have to
start testgroup
subtests with the prefix Test
.)
A valid subtest accepts a *testgroup.T
as its only argument and does not
return anything. If a subtest (exported method) has a different signature,
testgroup
will fail the parent test to avoid accidentally skipping malformed
tests.
testgroup
considers a few names to be hook methods that have special behavior.
You may find them useful to help clarify your code and avoid some repetition.
testgroup
supports the following hooks:
func (*MyGroup) PreGroup(t *testgroup.T) {
// code that will run before any of MyGroup's subtests have started
}
func (*MyGroup) PostGroup(t *testgroup.T) {
// code that will run after all of MyGroup's subtests have finished
}
func (*MyGroup) PreTest(t *testgroup.T) {
// code that will run at the beginning of each subtest in MyGroup
}
func (*MyGroup) PostTest(t *testgroup.T) {
// code that will run at the end of each subtest in MyGroup
}
Like subtests, these methods accept a single *testgroup.T
argument.
If you skip a test by calling t.Skip()
, the PreTest
and PostTest
hook
functions will still run before and after that test.
Here's an example of a top-level testing
-style test running the subtests in a
test group:
func TestMyGroup(t *testing.T) {
testgroup.RunSerially(t, &MyGroup{})
}
testgroup
has two ways to run subtests: RunSerially
and RunInParallel
.
RunSerially
runs a group's subtests in lexicographical order, one after
another.
Here's a contrived example:
func TestSerial(t *testing.T) {
testgroup.RunSerially(t, &MyGroup{})
}
type MyGroup struct{}
func (*MyGroup) C(t *testgroup.T) {}
func (*MyGroup) A(t *testgroup.T) {}
func (*MyGroup) B(t *testgroup.T) {}
Running the test above gives this output:
$ go test -v serial_test.go
=== RUN TestSerial
=== RUN TestSerial/A
=== RUN TestSerial/B
=== RUN TestSerial/C
--- PASS: TestSerial (0.00s)
--- PASS: TestSerial/A (0.00s)
--- PASS: TestSerial/B (0.00s)
--- PASS: TestSerial/C (0.00s)
PASS
ok command-line-arguments 0.014s
RunInParallel
runs a group's subtests in parallel.
When using this mode, you must not call t.Parallel()
inside your tests
โ testgroup
does this for you.
In order to make sure hooks run at the
correct time, RunInParallel
wraps a parent test around your subtests. By
default, the parent test is named _
, but you can override this by setting
RunInParallelParentTestName
.
The execution order of subtests and hooks looks like this:
- Run
PreGroup
. - In parallel, run the following sequence of steps for each subtest:
- Run
PreTest
. - Run the subtest method.
- Run
PostTest
.
- Run
- After all subtests finish, run
PostGroup
.
Here's another contrived example:
func TestParallel(t *testing.T) {
testgroup.RunInParallel(t, &MyGroup{})
}
type MyGroup struct{}
func (*MyGroup) C(t *testgroup.T) {}
func (*MyGroup) A(t *testgroup.T) {}
func (*MyGroup) B(t *testgroup.T) {}
Running the test above gives this output:
$ go test -v parallel_test.go
=== RUN TestParallel
=== RUN TestParallel/_
=== RUN TestParallel/_/A
=== PAUSE TestParallel/_/A
=== RUN TestParallel/_/B
=== PAUSE TestParallel/_/B
=== RUN TestParallel/_/C
=== PAUSE TestParallel/_/C
=== CONT TestParallel/_/A
=== CONT TestParallel/_/C
=== CONT TestParallel/_/B
--- PASS: TestParallel (0.00s)
--- PASS: TestParallel/_ (0.00s)
--- PASS: TestParallel/_/B (0.00s)
--- PASS: TestParallel/_/C (0.00s)
--- PASS: TestParallel/_/A (0.00s)
PASS
ok command-line-arguments 0.014s
testgroup.T
is a type passed to each test function. It is mainly concerned
with test state, and it embeds and contains other types for convenience.
testgroup.T.Run
is just like testing.T.Run
, but its test function has a
*testgroup.T
argument for convenience.
func (*MyGroup) MySubtest(t *testgroup.T) {
// set up a table of testcases
type testcase struct{
input, output int
}
table := []testcase{
// ...
}
for _, tc := range table {
tc := tc // local copy to pin range variable
t.Run(fmt.Sprintf("%d", tc.input), func (t *testgroup.T) {
t.Equal(tc.output, someCalculation(tc.input))
})
}
}
testgroup.T
has a few convenience methods that simply wrap the package-level
functions.
testgroup.T.RunSerially
callstestgroup.RunSerially
.testgroup.T.RunInParallel
callstestgroup.RunInParallel
.
testgroup.T
embeds a *testing.T
, which lets you write
func (*MyGroup) MySubtest(t *testgroup.T) {
if testing.Short() {
t.Skip("skipping due to short mode")
}
t.Logf("Have we failed yet? %v", t.Failed())
}
Similar to testify/suite
, testgroup.T
embeds a
testify/assert
*Assertions
, so you can call its
member functions directly from a testgroup.T
:
func (*MyGroup) MySubtest(t *testgroup.T) {
const expectedValue = 42
result := callSomeFunction(input)
t.Equal(expectedValue, result)
}
testgroup.T
also contains a testify/require
*Assertions
named Require
. You can use it to fail your test immediately
instead of continuing.
func (*MyGroup) MySubtest(t *testgroup.T) {
const expectedValue = 42
result, err := somethingThatMightError(input)
t.NoError(err) // testify/assert assertion -- continues test execution if it fails
t.Require.NotNil(result) // testify/require assertion -- stops test execution if it fails
t.Equal(expectedValue, result)
}
testgroup
has adopted a
Code of Conduct.
If you have any concerns about the Code or behavior which you have experienced
in the project, please contact us at [email protected].
We'd love to hear from you, whether you've found a bug or want to suggest how
testgroup
could be better. Please
open an issue and
let us know what you think!
If you want to contribute code to testgroup
, please be sure to read our
contribution guidelines.
We highly recommend opening an issue before you start working on your pull
request. We'd like to talk with you about the change you want to make before
you start making it. ๐
testgroup
is licensed under the Apache License, Version 2.0.
If you believe you have identified a security vulnerability in this project, please send an email to the project team at [email protected] detailing the suspected issue and any methods you've found to reproduce it.
Please do not open an issue in the GitHub repository, as we'd prefer to keep vulnerability reports private until we've had an opportunity to review and address them. Thank you.