Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Ability to run test sub-suites.
- Provide FuncPC for sub-tests.

## [1.1.0] - 2026-05-20
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Add some flavor to your tests with
- [Lifecycle hooks](./examples/02_hooks/main_test.go) - before and after any suite, test & sub-test.
- [Test annotations](./examples/07_annotations/main_test.go) - attach static options to any test.
- [Informative errors and traces](./examples/06_errors/main_test.go) - no need to guess what went wrong.
- Sub-tests - support for nested tests.
- Sub-tests & sub-suites - support for nested tests and nested suites.
- Test reflection - deeply inspect test's meta-information.
- Caching - key-value storage persistent between test runs.

Expand Down
21 changes: 21 additions & 0 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,24 @@ go test . -run ./MySuite -testo.m TestFoo

> [!TIP]
> See also [Visual Studio Code extension](../vscode-extension) which does just that for you.

## How to run sub-suites

There a `testo.RunSubSuite` function for that:

```go
type OuterSuite struct{ testo.Suite[T] }

func (OuterSuite) Test(t T) {
testo.RunSubSuite(t, new(InnerSuite))
}

type InnerSuite struct{ testo.Suite[T] }

func (InnerSuite) Test(t T) {
t.Log("Hello from sub-suite!")
}
```

> [!WARNING]
> Running the same suite as sub-suite may cause infinite loop.
4 changes: 4 additions & 0 deletions examples/08_subsuites/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MAKEFLAGS += --always-make

test:
go test . -v -tags example -count=1
27 changes: 27 additions & 0 deletions examples/08_subsuites/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build example

package main

import (
"testing"

"github.com/ozontech/testo"
)

type T = *testo.T

type OuterSuite struct{ testo.Suite[T] }

func (OuterSuite) Test(t T) {
testo.RunSubSuite(t, new(InnerSuite))
}

type InnerSuite struct{ testo.Suite[T] }

func (InnerSuite) Test(t T) {
t.Log("Hello from sub-suite!")
}

func Test(t *testing.T) {
testo.RunSuite(t, new(OuterSuite))
}
17 changes: 17 additions & 0 deletions examples/08_subsuites/output.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
=== RUN Test
=== RUN Test/OuterSuite
=== RUN Test/OuterSuite/testo!
=== RUN Test/OuterSuite/testo!/Test
=== RUN Test/OuterSuite/testo!/Test/InnerSuite
=== RUN Test/OuterSuite/testo!/Test/InnerSuite/testo!
=== RUN Test/OuterSuite/testo!/Test/InnerSuite/testo!/Test
main_test.go:22: Hello from sub-suite!
--- PASS: Test (0.00s)
--- PASS: Test/OuterSuite (0.00s)
--- PASS: Test/OuterSuite/testo! (0.00s)
--- PASS: Test/OuterSuite/testo!/Test (0.00s)
--- PASS: Test/OuterSuite/testo!/Test/InnerSuite (0.00s)
--- PASS: Test/OuterSuite/testo!/Test/InnerSuite/testo! (0.00s)
--- PASS: Test/OuterSuite/testo!/Test/InnerSuite/testo!/Test (0.00s)
PASS
ok github.com/ozontech/testo/examples/08_subsuites 0.666s
24 changes: 23 additions & 1 deletion runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,26 @@ func RunSuite[Suite suite[T], T CommonT](

r := newRunner[Suite]()

return r.runSuite(testingT, suite, options...)
return r.runSuite(testingT, suite, nil, options...)
}

// RunSubSuite runs a sub-suite.
//
// This is similar to [RunSuite] but designed to be called from other suites.
//
// RunSubSuite reports whether all sub-suite tests succeeded.
//
// NOTE: this function may cause infinite loop if called within the same suite as passed to it.
func RunSubSuite[Suite suite[Sub], Parent, Sub CommonT](
t Parent,
suite Suite,
options ...testoplugin.Option,
) bool {
t.Helper()

r := newRunner[Suite]()

return r.runSuite(t.unwrap().testingT, suite, &t.unwrap().reflection.Suite, options...)
}

// Run runs f as a subtest of t called name. It runs f in a separate goroutine
Expand Down Expand Up @@ -130,6 +149,7 @@ func (r *runner[Suite, T]) collectTests(t TestingT, caller string) suiteTests[Su
func (r *runner[Suite, T]) runSuite(
testingT TestingT,
suite Suite,
parentSuite *testoreflect.SuiteInfo,
options ...testoplugin.Option,
) bool {
testingT.Helper()
Expand All @@ -141,6 +161,7 @@ func (r *runner[Suite, T]) runSuite(
tests := r.collectTests(testingT, caller)

suiteInfo := testoreflect.SuiteInfo{
Parent: parentSuite,
Name: r.suiteName,
Caller: testingT.Name(),
TestingT: testingT,
Expand Down Expand Up @@ -198,6 +219,7 @@ func (r *runner[Suite, T]) runSuiteTests(t T, s Suite, tests suiteTests[Suite, T
s.BeforeAll(t)

suiteInfo := testoreflect.SuiteInfo{
Parent: t.unwrap().reflection.Suite.Parent,
Name: t.unwrap().reflection.Suite.Name,
Caller: t.unwrap().reflection.Suite.Caller,
TestingT: t.unwrap().reflection.Suite.TestingT,
Expand Down
2 changes: 2 additions & 0 deletions suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type suite[T CommonT] interface {
private()
}

var _ suite[*T] = (*Suite[*T])(nil)

// Suite is the base suite that all user-defined
// suites must embed.
//
Expand Down
26 changes: 26 additions & 0 deletions suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,29 @@ func BenchmarkCasesPermutations(b *testing.B) {
}
})
}

type SubSuiteParent struct {
Suite[*T]
}

func (s SubSuiteParent) Test(t *T) {
if !RunSubSuite(t, new(SubSuiteChild)) {
t.Fatal("run sub suite failed")
}
}

type SubSuiteChild struct{ Suite[*T] }

func (s SubSuiteChild) Test(t *T) {
if reflect.TypeOf(Reflect(t).Suite.Parent.Value) != reflect.TypeFor[*SubSuiteParent]() {
t.Fatal("unexpected parent suite type")
}
}

func TestSubSuite(t *testing.T) {
t.Parallel()

if !RunSuite(t, new(SubSuiteParent)) {
t.Fatal("run suite failed")
}
}
4 changes: 4 additions & 0 deletions testoreflect/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ func (ParametrizedTestInfo) isTestInfo() {}

// SuiteInfo is the information about suite.
type SuiteInfo struct {
// Parent refers to the parent suite info.
// Non-nil value means that current suite is sub-suite.
Parent *SuiteInfo

// Name of this suite.
Name string

Expand Down
Loading