From 550426668d5eb0db48101fe48e897ac5f757dc3c Mon Sep 17 00:00:00 2001 From: Tronje Krop Date: Fri, 5 Jun 2026 01:07:45 +0200 Subject: [PATCH] feat: improve test context (#149) Signed-off-by: Tronje Krop --- Makefile | 2 +- README.md | 56 +++- go.mod | 2 +- mock/mocks_test.go | 4 +- reflect/reflect_test.go | 674 +++++++++++++++++++++++----------------- test/README.md | 113 ++++--- test/benchmark_test.go | 16 + test/caller_test.go | 2 +- test/common.go | 18 -- test/common_test.go | 347 --------------------- test/context.go | 536 +++++++++++++++++++------------- test/context_test.go | 460 +++++++++++++++++++++++++-- test/factory.go | 40 +-- test/factory_test.go | 65 ++-- test/pattern.go | 10 + test/pattern_test.go | 154 +++++++-- test/reporter.go | 4 +- test/reporter_test.go | 22 +- 18 files changed, 1490 insertions(+), 1035 deletions(-) delete mode 100644 test/common.go delete mode 100644 test/common_test.go diff --git a/Makefile b/Makefile index dc85a63..ce44863 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ TMPDIR ?= /tmp # Setup default go-make installation flags. INSTALL_FLAGS ?= -mod=readonly -buildvcs=auto # Setup go-make version to use desired build and config scripts. -GOMAKE_DEP ?= github.com/tkrop/go-make@v0.4.8 +GOMAKE_DEP ?= github.com/tkrop/go-make@v0.4.9 # Request targets from go-make show-targets target. TARGETS := $(shell command -v $(GOBIN)/go-make >/dev/null || \ $(GO) install $(INSTALL_FLAGS) $(GOMAKE_DEP) >&2 && \ diff --git a/README.md b/README.md index 141e997..ecd0a36 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,43 @@ You can find more information in the [`go-testing` documentation][go-testing]. [unit-testing]: -### Example Usage +### Example usage -First you have to define a unified test/benchmark parameter set. While this can -be done in many different ways, the following setup structure is considered to -be the [`go-testing`][go-testing] idiomatic way due to its wide coverage of -different use cases, its flexibility, and its non-the-last readability: +A minimal example of a *strongly isolated* and *parallel running* unit-test +using the [`test`][test] package of [`go-testing`][go-testing] will simply +create a test function and run it via [`test.Run`][run]: + +```go +func TestUnit(t *testing.T) { + t.Parallel() + + t.Run("test-case-name", test.Run(func(t test.Tester) { + // Given + unit := NewUnitService(...) + + // When + result, err := unit.call(param.input*...) + + // Then + assert.Equal(t, ..., err) + assert.Equal(t, ..., result) + })) + + ... // more test cases +} +``` + +**Note:** if you only have a single test case, you can also directly run the +test function via [`test.Run(...)(t)`][run] in the parent test context. + +However, the true power of the framework is unleashed when you define a +systematic set of unified test cases running in parallel with detailed mock +setups and strong validation of the system under test. + +To accomplish this, you first have to define a unified test parameter set. +While this can be done in many different ways, the following test setup is +considered to be the [`go-testing`][go-testing] idiomatic way due to its wide +coverage of different use cases, its flexibility, and its high readability: ```go type UnitParams struct { @@ -114,11 +145,11 @@ var unitTestCases = map[string]UnitParams { } ``` -Now you can set up a *strongly isolated* and *parallel running* test. While +Now you can set up the *strongly isolated* and *parallel running* test. While there are again many ways to define tests (see package [test](test)), the -following pattern is considered to be the most [`go-testing`][go-testing] -idiomatic way again due to its wide coverage of different use cases, its -flexibility, and its non-the-last readability: +following [`test.Factory`][factory] based [`test.Map(t, cases)`][map] pattern +is considered to be the most idiomatic way again due to its wide coverage of +different use cases, its flexibility, and its high readability: ```go func TestUnit(t *testing.T) { @@ -155,9 +186,8 @@ func TestUnit(t *testing.T) { As an addon, you can also use the same pattern to define benchmarks for a system under test based on the before defined test parameter set. The following -setup structure is considered to be the most [`go-testing`][go-testing] -framework idiomatic way (see also [Test benchmark -setup](test#parameterized-benchmark-setup)): +setup structure is considered to be the most framework idiomatic way (see also +[Test benchmark setup](test#parameterized-benchmark-setup)): ```go func BenchmarkUnit(b *testing.B) { @@ -196,6 +226,8 @@ the the copy nature of the `runtime.KeepAlive`. For more test patterns and variations have a closer look at details in the [test](test) package or read the [package docs][docs-test]. +[map]: +[factory]: [docs-test]: diff --git a/go.mod b/go.mod index 039c0de..11ebd57 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tkrop/go-testing -go 1.26.3 +go 1.26.4 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc diff --git a/mock/mocks_test.go b/mock/mocks_test.go index 98365d1..8f8d18c 100644 --- a/mock/mocks_test.go +++ b/mock/mocks_test.go @@ -149,7 +149,7 @@ func TestMocks(t *testing.T) { mocks := mock.NewMocks(t) // When - test.InRun(test.Success, func(tt test.Test) { + test.New(t).Run("", func(tt test.Test) { // Given imocks := mock.NewMocks(tt) if param.misses != nil { @@ -163,7 +163,7 @@ func TestMocks(t *testing.T) { // When param.call(tt, imocks) - })(t) + }) }) } diff --git a/reflect/reflect_test.go b/reflect/reflect_test.go index 8636947..ade3fb4 100644 --- a/reflect/reflect_test.go +++ b/reflect/reflect_test.go @@ -11,39 +11,52 @@ import ( "github.com/tkrop/go-testing/test" ) -type Struct struct { - s string - a any -} +// Values used in the tests. +var ( + structInit = newStruct("init", "init") + structEmpty = newStruct("", nil) + structFinal = newStruct("set final", "set final") + structPtrInit = newPtrStruct("init", "init") + structPtrEmpty = newPtrStruct("", nil) + structPtrFinal = newPtrStruct("set final", "set final") +) +// Types used in the tests. type ( - IntAlias int - StructPtrAlias *Struct + // intAlias is a test type alias for int. + intAlias int + // structPtrAlias is a test type alias for *Struct. + structPtrAlias *structAny + // structAny is a test struct type used for testing the `Builder` interface + // with struct targets. + structAny struct { + s string + a any + } ) -func NewStruct(s string, a any) Struct { return Struct{s: s, a: a} } -func NewPtrStruct(s string, a any) *Struct { return &Struct{s: s, a: a} } +// newStruct creates a new instance of `Struct` with the given string and any +// value. +func newStruct(s string, a any) structAny { return structAny{s: s, a: a} } -var ( - structInit = NewStruct("init", "init") - structEmpty = NewStruct("", nil) - structFinal = NewStruct("set final", "set final") - structPtrInit = NewPtrStruct("init", "init") - structPtrEmpty = NewPtrStruct("", nil) - structPtrFinal = NewPtrStruct("set final", "set final") -) +// newPtrStruct creates a new instance of `*Struct` with the given string and +// any value. +func newPtrStruct(s string, a any) *structAny { return &structAny{s: s, a: a} } -type BuilderStructParams struct { - target Struct - setup func(reflect.Builder[Struct]) +// builderStructParams is a test parameter type for testing the `Builder` +// interface with struct targets. +type builderStructParams struct { + target structAny + setup func(reflect.Builder[structAny]) expect mock.SetupFunc - check func(test.Test, reflect.Builder[Struct]) + check func(test.Test, reflect.Builder[structAny]) } -var builderStructTestCases = map[string]BuilderStructParams{ +// Test cases for testing the `Builder` interface with struct targets. +var builderStructTestCases = map[string]builderStructParams{ "struct-get-init": { target: structInit, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "init", b.Get("s")) assert.Equal(t, "init", b.Get("a")) assert.Equal(t, "init", b.Find("default", "s")) @@ -58,7 +71,7 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-get-invalid": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Get("invalid") }, expect: test.Panic("target field not found [invalid]"), @@ -66,7 +79,7 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-set-invalid": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("invalid", "set final") }, expect: test.Panic("target field not found [invalid]"), @@ -74,11 +87,11 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-set-compatible": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structFinal, b.Get("")) @@ -88,7 +101,7 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-set-non-compatible": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", []byte("set final")) }, expect: test.Panic("value must be compatible [[]uint8 => string]"), @@ -96,11 +109,11 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-set": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structFinal, b.Get("")) @@ -110,11 +123,11 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-set-nil": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set any").Set("a", "set any"). Set("s", nil).Set("a", nil) }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) assert.Equal(t, structEmpty, b.Get("")) @@ -124,21 +137,21 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-reset-no-pointer": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", structFinal) }, expect: test.Panic("target must be compatible struct pointer " + - "[reflect_test.Struct => *reflect_test.Struct]"), + "[reflect_test.structAny => *reflect_test.structAny]"), }, "struct-reset-pointer": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structFinal, b.Get("")) @@ -148,11 +161,11 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-reset-any-nil": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", nil) }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) assert.Equal(t, structEmpty, b.Get("")) @@ -162,11 +175,11 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-reset-struct-nil": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). - Set("", (*Struct)(nil)) + Set("", (*structAny)(nil)) }, - check: func(t test.Test, b reflect.Builder[Struct]) { + check: func(t test.Test, b reflect.Builder[structAny]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) assert.Equal(t, structEmpty, b.Get("")) @@ -176,18 +189,20 @@ var builderStructTestCases = map[string]BuilderStructParams{ "struct-reset-any-invalid": { target: structInit, - setup: func(b reflect.Builder[Struct]) { + setup: func(b reflect.Builder[structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", struct{}{}) }, expect: test.Panic("target must be compatible struct pointer " + - "[struct {} => *reflect_test.Struct]"), + "[struct {} => *reflect_test.structAny]"), }, } +// TestBuilderStruct tests the `Builder` interface with struct targets using +// various test cases defined in the `builderStructTestCases` map. func TestBuilderStruct(t *testing.T) { test.Map(t, builderStructTestCases). - Run(func(t test.Test, param BuilderStructParams) { + Run(func(t test.Test, param builderStructParams) { // Given mock.NewMocks(t).Expect(param.expect) accessor := reflect.NewAccessor(param.target) @@ -202,18 +217,21 @@ func TestBuilderStruct(t *testing.T) { }) } -type BuilderPtrStructParams struct { - target *Struct - setup func(reflect.Builder[*Struct]) +// BuilderAnyParams is a test parameter type for testing the `Builder` +// interface with any type of target. +type builderPtrStructParams struct { + target *structAny + setup func(reflect.Builder[*structAny]) expect mock.SetupFunc - check func(test.Test, reflect.Builder[*Struct]) + check func(test.Test, reflect.Builder[*structAny]) } -var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ +// Test cases for testing the `Builder` interface with struct pointer targets. +var builderPtrStructTestCases = map[string]builderPtrStructParams{ // Test cases for nil interface pointer. "nil-any-get": { target: nil, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) assert.Equal(t, "", b.Find("default", "s")) @@ -228,7 +246,7 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-get-invalid": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Get("invalid") }, expect: test.Panic("target field not found [invalid]"), @@ -236,7 +254,7 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-set-invalid": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("invalid", "set final") }, expect: test.Panic("target field not found [invalid]"), @@ -244,11 +262,11 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-set": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -258,11 +276,11 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-set-compatible": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -272,7 +290,7 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-set-non-compatible": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", []byte("set final")) }, expect: test.Panic("value must be compatible [[]uint8 => string]"), @@ -280,11 +298,11 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-reset": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -294,29 +312,29 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-any-reset-nil": { target: nil, - setup: func(b reflect.Builder[*Struct]) { - b.Set("", (*Struct)(nil)) + setup: func(b reflect.Builder[*structAny]) { + b.Set("", (*structAny)(nil)) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { - assert.Equal(t, (*Struct)(nil), b.Get("")) - assert.Equal(t, (*Struct)(nil), b.Build()) + check: func(t test.Test, b reflect.Builder[*structAny]) { + assert.Equal(t, (*structAny)(nil), b.Get("")) + assert.Equal(t, (*structAny)(nil), b.Build()) }, }, "nil-any-reset-invalid": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", struct{}{}) }, expect: test.Panic("target must be compatible struct pointer " + - "[struct {} => *reflect_test.Struct]"), + "[struct {} => *reflect_test.structAny]"), }, "nil-any-reset-nil-invalid": { target: nil, - setup: func(b reflect.Builder[*Struct]) { - b.Set("", (*Struct)(nil)) + setup: func(b reflect.Builder[*structAny]) { + b.Set("", (*structAny)(nil)) b.Get("invalid") }, expect: test.Panic("target field not found [invalid]"), @@ -324,8 +342,8 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ // Test cases for nil struct pointer. "nil-struct-get": { - target: new(Struct), - check: func(t test.Test, b reflect.Builder[*Struct]) { + target: new(structAny), + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) assert.Equal(t, "", b.Find("default", "s")) @@ -339,28 +357,28 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "nil-struct-get-invalid": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Get("invalid") }, expect: test.Panic("target field not found [invalid]"), }, "nil-struct-set-invalid": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Set("invalid", "set final") }, expect: test.Panic("target field not found [invalid]"), }, "nil-struct-set": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -369,12 +387,12 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "nil-struct-set-compatible": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -383,20 +401,20 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "nil-struct-set-non-compatible": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", []byte("set final")) }, expect: test.Panic("value must be compatible [[]uint8 => string]"), }, "nil-struct-reset": { - target: new(Struct), - setup: func(b reflect.Builder[*Struct]) { + target: new(structAny), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -406,18 +424,18 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ "nil-struct-reset-invalid": { target: nil, - setup: func(b reflect.Builder[*Struct]) { + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", struct{}{}) }, expect: test.Panic("target must be compatible struct pointer " + - "[struct {} => *reflect_test.Struct]"), + "[struct {} => *reflect_test.structAny]"), }, // Test cases for struct pointer instance. "ptr-get": { - target: NewPtrStruct("init", "init"), - check: func(t test.Test, b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "init", b.Get("s")) assert.Equal(t, "init", b.Get("a")) assert.Equal(t, "init", b.Find("default", "s")) @@ -431,20 +449,20 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "ptr-get-invalid": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Get("invalid") }, expect: test.Panic("target field not found [invalid]"), }, "ptr-set": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -453,12 +471,12 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "ptr-set-compatible": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -467,20 +485,20 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "ptr-set-non-compatible": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", []byte("set final")) }, expect: test.Panic("value must be compatible [[]uint8 => string]"), }, "ptr-reset": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) }, - check: func(t test.Test, b reflect.Builder[*Struct]) { + check: func(t test.Test, b reflect.Builder[*structAny]) { assert.Equal(t, "set final", b.Get("s")) assert.Equal(t, "set final", b.Get("a")) assert.Equal(t, structPtrFinal, b.Get("")) @@ -489,19 +507,22 @@ var builderPtrStructTestCases = map[string]BuilderPtrStructParams{ }, "ptr-reset-invalid": { - target: NewPtrStruct("init", "init"), - setup: func(b reflect.Builder[*Struct]) { + target: newPtrStruct("init", "init"), + setup: func(b reflect.Builder[*structAny]) { b.Set("s", "set first").Set("a", "set first"). Set("", struct{}{}) }, expect: test.Panic("target must be compatible struct pointer " + - "[struct {} => *reflect_test.Struct]"), + "[struct {} => *reflect_test.structAny]"), }, } +// TestBuilderPtrStruct tests the `Builder` interface with struct pointer +// targets using various test cases defined in the `builderPtrStructTestCases` +// map. func TestBuilderPtrStruct(t *testing.T) { test.Map(t, builderPtrStructTestCases). - Run(func(t test.Test, param BuilderPtrStructParams) { + Run(func(t test.Test, param builderPtrStructParams) { // Given mock.NewMocks(t).Expect(param.expect) accessor := reflect.NewAccessor(param.target) @@ -516,14 +537,17 @@ func TestBuilderPtrStruct(t *testing.T) { }) } -type BuilderAnyParams struct { +// builderAnyParams is a test parameter type for testing the Builder interface +// with any type of target. +type builderAnyParams struct { target any setup func(reflect.Builder[any]) expect mock.SetupFunc check func(test.Test, reflect.Builder[any]) } -var builderAnyTestCases = map[string]BuilderAnyParams{ +// Test cases for testing the Builder interface with any type of target. +var builderAnyTestCases = map[string]builderAnyParams{ // Test cases for invalid types. "invalid-type-nil": { target: nil, @@ -627,7 +651,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ Set("", structFinal) }, expect: test.Panic("target must be compatible struct pointer " + - "[reflect_test.Struct => *reflect_test.Struct]"), + "[reflect_test.structAny => *reflect_test.structAny]"), }, "struct-reset-invalid": { @@ -637,12 +661,12 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ Set("", struct{}{}) }, expect: test.Panic("target must be compatible struct pointer " + - "[struct {} => *reflect_test.Struct]"), + "[struct {} => *reflect_test.structAny]"), }, // Test cases for struct pointer instance. "ptr-get": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), check: func(t test.Test, b reflect.Builder[any]) { assert.Equal(t, "init", b.Get("s")) assert.Equal(t, "init", b.Get("a")) @@ -657,7 +681,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "ptr-get-invalid": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), setup: func(b reflect.Builder[any]) { b.Get("invalid") }, @@ -665,7 +689,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "ptr-set-invalid": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), setup: func(b reflect.Builder[any]) { b.Set("invalid", "set final") }, @@ -673,7 +697,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "ptr-set": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), setup: func(b reflect.Builder[any]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") @@ -687,7 +711,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "ptr-set-compatible": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), setup: func(b reflect.Builder[any]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) @@ -709,7 +733,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "ptr-reset": { - target: NewPtrStruct("init", "init"), + target: newPtrStruct("init", "init"), setup: func(b reflect.Builder[any]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) @@ -724,7 +748,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ // Test cases for nil struct pointer instance. "nil-get": { - target: new(Struct), + target: new(structAny), check: func(t test.Test, b reflect.Builder[any]) { assert.Equal(t, "", b.Get("s")) assert.Equal(t, nil, b.Get("a")) @@ -739,7 +763,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "nil-get-invalid": { - target: new(Struct), + target: new(structAny), setup: func(b reflect.Builder[any]) { b.Get("invalid") }, @@ -747,7 +771,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "nil-set-invalid": { - target: new(Struct), + target: new(structAny), setup: func(b reflect.Builder[any]) { b.Set("invalid", "set final") }, @@ -755,7 +779,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "nil-set": { - target: new(Struct), + target: new(structAny), setup: func(b reflect.Builder[any]) { b.Set("s", "set first").Set("a", "set first"). Set("s", "set final").Set("a", "set final") @@ -769,7 +793,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "nil-set-compatible": { - target: new(Struct), + target: new(structAny), setup: func(b reflect.Builder[any]) { b.Set("s", string([]byte("set final"))). Set("a", string([]byte("set final"))) @@ -791,7 +815,7 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, "nil-reset": { - target: new(Struct), + target: new(structAny), setup: func(b reflect.Builder[any]) { b.Set("s", "set first").Set("a", "set first"). Set("", structPtrFinal) @@ -805,9 +829,10 @@ var builderAnyTestCases = map[string]BuilderAnyParams{ }, } +// TestBuilderAny tests the Builder interface with any type of target. func TestBuilderAny(t *testing.T) { test.Map(t, builderAnyTestCases). - Run(func(t test.Test, param BuilderAnyParams) { + Run(func(t test.Test, param builderAnyParams) { // Given mock.NewMocks(t).Expect(param.expect) accessor := reflect.NewAccessor(param.target) @@ -817,179 +842,231 @@ func TestBuilderAny(t *testing.T) { param.setup(accessor) } - // The + // Then param.check(t, accessor) }) } -//revive:disable-next-line:function-length // Test suite approach. -func TestNewBuilder(t *testing.T) { - t.Parallel() +// newBuilderParams is a test parameter type for testing the constructor +// functions of the reflect package. +type newBuilderParams struct { + setup mock.SetupFunc + call func() any + check func(t test.Test, b any) +} + +// newBuilderTestCases is a map of test parameters for testing the constructor +// functions of the reflect package. +var newBuilderTestCases = map[string]newBuilderParams{ + "builder-ptr-alias-panic": { + setup: test.Panic(fmt.Sprintf( + "cast failed [%T]: %v", structPtrAlias(nil), + newPtrStruct("", nil))), + call: func() any { + return reflect.NewBuilder[structPtrAlias]() + }, + }, + + "builder-struct": { + call: func() any { + b := reflect.NewBuilder[structAny]() + b.Set("s", "set final").Set("a", "set final") + + return b + }, + check: func(t test.Test, b any) { + builder := test.Cast[reflect.Builder[structAny]](b) + + assert.Equal(t, "set final", builder.Get("s")) + assert.Equal(t, "set final", builder.Get("a")) + assert.Equal(t, structFinal, builder.Get("")) + assert.Equal(t, structFinal, builder.Build()) + }, + }, + + "builder-ptr": { + call: func() any { + b := reflect.NewBuilder[*structAny]() + b.Set("s", "set final").Set("a", "set final") + + return b + }, + check: func(t test.Test, b any) { + builder := test.Cast[reflect.Builder[*structAny]](b) + + assert.Equal(t, "set final", builder.Get("s")) + assert.Equal(t, "set final", builder.Get("a")) + assert.Equal(t, structPtrFinal, builder.Get("")) + assert.Equal(t, structPtrFinal, builder.Build()) + }, + }, + + "setter-nil": { + call: func() any { + s := reflect.NewSetter((*structAny)(nil)) + s.Set("s", "set final").Set("a", "set final") + + return s + }, + check: func(t test.Test, b any) { + s := test.Cast[reflect.Setter[*structAny]](b) + + assert.Equal(t, structPtrFinal, s.Build()) + }, + }, + + "setter-struct": { + call: func() any { + s := reflect.NewSetter(newStruct("init", "init")) + s.Set("s", "set final").Set("a", "set final") + + return s + }, + check: func(t test.Test, b any) { + s := test.Cast[reflect.Setter[structAny]](b) + + assert.Equal(t, structFinal, s.Build()) + }, + }, + + "setter-ptr": { + call: func() any { + s := reflect.NewSetter(newPtrStruct("init", "init")) + s.Set("s", "set final").Set("a", "set final") + + return s + }, + check: func(t test.Test, b any) { + s := test.Cast[reflect.Setter[*structAny]](b) + + assert.Equal(t, structPtrFinal, s.Build()) + }, + }, - t.Run("builder-ptr-alias-panic", - test.Run(test.Success, func(t test.Test) { - mock.NewMocks(t).Expect(test.Panic(fmt.Sprintf( - "cast failed [%T]: %v", StructPtrAlias(nil), - NewPtrStruct("", nil)))) + "getter-nil": { + call: func() any { + return reflect.NewGetter((*structAny)(nil)) + }, + check: func(t test.Test, b any) { + g := test.Cast[reflect.Getter[*structAny]](b) - _ = reflect.NewBuilder[StructPtrAlias]() - })) + assert.Equal(t, "", g.Get("s")) + assert.Equal(t, nil, g.Get("a")) + assert.Equal(t, structPtrEmpty, g.Get("")) + }, + }, - t.Run("builder-struct", func(t *testing.T) { - t.Parallel() - // Given - b := reflect.NewBuilder[Struct]() + "getter-struct": { + call: func() any { + return reflect.NewGetter(structFinal) + }, + check: func(t test.Test, b any) { + g := test.Cast[reflect.Getter[structAny]](b) - // When - b.Set("s", "set final").Set("a", "set final") + assert.Equal(t, "set final", g.Get("s")) + assert.Equal(t, "set final", g.Get("a")) + assert.Equal(t, structFinal, g.Get("")) + }, + }, - // Then - assert.Equal(t, "set final", b.Get("s")) - assert.Equal(t, "set final", b.Get("a")) - assert.Equal(t, structFinal, b.Get("")) - assert.Equal(t, structFinal, b.Build()) - }) + "getter-ptr": { + call: func() any { + return reflect.NewGetter(structPtrFinal) + }, + check: func(t test.Test, b any) { + g := test.Cast[reflect.Getter[*structAny]](b) - t.Run("builder-ptr", func(t *testing.T) { - t.Parallel() - // Given - b := reflect.NewBuilder[*Struct]() + assert.Equal(t, "set final", g.Get("s")) + assert.Equal(t, "set final", g.Get("a")) + assert.Equal(t, structPtrFinal, g.Get("")) + }, + }, - // When - b.Set("s", "set final").Set("a", "set final") + "finder-nil": { + call: func() any { + return reflect.NewFinder((*structAny)(nil)) + }, + check: func(t test.Test, b any) { + f := test.Cast[reflect.Finder[*structAny]](b) - // Then - assert.Equal(t, "set final", b.Get("s")) - assert.Equal(t, "set final", b.Get("a")) - assert.Equal(t, structPtrFinal, b.Get("")) - assert.Equal(t, structPtrFinal, b.Build()) - }) - - t.Run("setter-nil", func(t *testing.T) { - t.Parallel() - - // Given - s := reflect.NewSetter((*Struct)(nil)) + assert.Equal(t, "", f.Find("default", "s")) + assert.Equal(t, "default", f.Find("default", "a")) + assert.Equal(t, "", f.Find("default")) + assert.Equal(t, "", f.Find("default", "*")) + assert.Equal(t, "default", f.Find("default", "x")) + }, + }, + + "finder-struct": { + call: func() any { + return reflect.NewFinder(structFinal) + }, + check: func(t test.Test, b any) { + f := test.Cast[reflect.Finder[structAny]](b) + + assert.Equal(t, "set final", f.Find("default", "s")) + assert.Equal(t, "default", f.Find("default", "a")) + assert.Equal(t, "set final", f.Find("default")) + assert.Equal(t, "set final", f.Find("default", "*")) + assert.Equal(t, "default", f.Find("default", "x")) + }, + }, + + "finder-ptr": { + call: func() any { + return reflect.NewFinder(structPtrFinal) + }, + check: func(t test.Test, b any) { + f := test.Cast[reflect.Finder[*structAny]](b) - // When - s.Set("s", "set final").Set("a", "set final") + assert.Equal(t, "set final", f.Find("default", "s")) + assert.Equal(t, "default", f.Find("default", "a")) + assert.Equal(t, "set final", f.Find("default")) + assert.Equal(t, "set final", f.Find("default", "*")) + assert.Equal(t, "default", f.Find("default", "x")) + }, + }, +} - // ThenstructEmpty - assert.Equal(t, structPtrFinal, s.Build()) - }) - - t.Run("setter-struct", func(t *testing.T) { - t.Parallel() - // Given - s := reflect.NewSetter(NewStruct("init", "init")) - - // When - s.Set("s", "set final").Set("a", "set final") - - // Then - assert.Equal(t, structFinal, s.Build()) - }) - - t.Run("setter-ptr", func(t *testing.T) { - t.Parallel() - - // Given - s := reflect.NewSetter(NewPtrStruct("init", "init")) - - // When - s.Set("s", "set final").Set("a", "set final") - - // Then - assert.Equal(t, structPtrFinal, s.Build()) - }) - - t.Run("getter-nil", func(t *testing.T) { - t.Parallel() - - // Given - g := reflect.NewGetter((*Struct)(nil)) - - // Then - assert.Equal(t, "", g.Get("s")) - assert.Equal(t, nil, g.Get("a")) - assert.Equal(t, structPtrEmpty, g.Get("")) - }) - - t.Run("getter-struct", func(t *testing.T) { - t.Parallel() - - // Given - g := reflect.NewGetter(structFinal) - - // Then - assert.Equal(t, "set final", g.Get("s")) - assert.Equal(t, "set final", g.Get("a")) - assert.Equal(t, structFinal, g.Get("")) - }) - - t.Run("getter-ptr", func(t *testing.T) { - t.Parallel() - - // Given - g := reflect.NewGetter(structPtrFinal) - - // Then - assert.Equal(t, "set final", g.Get("s")) - assert.Equal(t, "set final", g.Get("a")) - assert.Equal(t, structPtrFinal, g.Get("")) - }) - - t.Run("finder-nil", func(t *testing.T) { - t.Parallel() - - // Given - f := reflect.NewFinder((*Struct)(nil)) - - // Then - assert.Equal(t, "", f.Find("default", "s")) - assert.Equal(t, "default", f.Find("default", "a")) - assert.Equal(t, "", f.Find("default")) - assert.Equal(t, "", f.Find("default", "*")) - assert.Equal(t, "default", f.Find("default", "x")) - }) - - t.Run("finder-struct", func(t *testing.T) { - t.Parallel() - - // Given - f := reflect.NewFinder(structFinal) - - // Then - assert.Equal(t, "set final", f.Find("default", "s")) - assert.Equal(t, "default", f.Find("default", "a")) - assert.Equal(t, "set final", f.Find("default")) - assert.Equal(t, "set final", f.Find("default", "*")) - assert.Equal(t, "default", f.Find("default", "x")) - }) +// TestNewBuilder tests the constructor functions of the reflect package using +// various test cases defined in the `newBuilderTestCases` map. +func TestNewBuilder(t *testing.T) { + test.Map(t, newBuilderTestCases). + Run(func(t test.Test, param newBuilderParams) { + // Given + mock.NewMocks(t).Expect(param.setup) - t.Run("finder-ptr", func(t *testing.T) { - t.Parallel() + // When + b := param.call() - // Given - f := reflect.NewFinder(structPtrFinal) - - // Then - assert.Equal(t, "set final", f.Find("default", "s")) - assert.Equal(t, "default", f.Find("default", "a")) - assert.Equal(t, "set final", f.Find("default")) - assert.Equal(t, "set final", f.Find("default", "*")) - assert.Equal(t, "default", f.Find("default", "x")) - }) + // Then + if param.check != nil { + param.check(t, b) + } + }) } -type FindParams struct { +// findParams is a test parameter type for testing the `Find` function of the +// reflect package. +type findParams struct { + setup mock.SetupFunc param any deflt any names []string + call func(findParams) any expect any } -var findTestCases = map[string]FindParams{ +// findCall is a default call function for testing the `Find` function of the +// reflect package when a custom call function is not provided in the test +// parameters. +var findCall = func(param findParams) any { + return reflect.Find(param.param, param.deflt, param.names...) +} + +// findTestCases is a map of test parameters for testing the `Find` function +// of the reflect package. +var findTestCases = map[string]findParams{ // Test cases for values. "int": { param: 1, @@ -1061,41 +1138,54 @@ var findTestCases = map[string]FindParams{ names: []string{"invalid", "*"}, expect: "init", }, + + // Test cases for panic scenarios. + "panic-int-alias": { + setup: test.Panic("cast failed [int]: 1"), + call: func(_ findParams) any { + return reflect.Find[any](intAlias(1), 0) + }, + }, + "panic-ptr-alias": { + setup: test.Panic(fmt.Sprintf( + "cast failed [%T]: %v", structPtrAlias(nil), new(structAny))), + param: new(structAny), + call: func(_ findParams) any { + return reflect.Find[*structAny, structPtrAlias](new(structAny), nil) + }, + }, } +// TestFind tests the `Find` function of the reflect package using various test +// cases defined in the `findTestCases` map. func TestFind(t *testing.T) { - t.Run("kind-match-with-type-alias-panics", - test.Run(test.Success, func(t test.Test) { - mock.NewMocks(t).Expect(test.Panic("cast failed [int]: 1")) - - _ = reflect.Find[any](IntAlias(1), 0) - })) - - t.Run("kind-match-with-pointer-type-alias-panics", - test.Run(test.Success, func(t test.Test) { - mock.NewMocks(t).Expect(test.Panic(fmt.Sprintf( - "cast failed [%T]: %v", StructPtrAlias(nil), new(Struct)))) - - _ = reflect.Find[*Struct, StructPtrAlias](new(Struct), nil) - })) - test.Map(t, findTestCases). - Run(func(t test.Test, param FindParams) { + Run(func(t test.Test, param findParams) { + // Given + mock.NewMocks(t).Expect(param.setup) + if param.call == nil { + param.call = findCall + } + // When - expect := reflect.Find(param.param, param.deflt, param.names...) + expect := param.call(param) // Then assert.Equal(t, param.expect, expect) }) } -type NameParams struct { +// nameParams is a test parameter type for testing the `Name` function of the +// reflect package. +type nameParams struct { name string param any expect string } -var nameTestCases = map[string]NameParams{ +// nameTestCases is a map of test parameters for testing the `Name` function of +// the reflect package. +var nameTestCases = map[string]nameParams{ // Empty names. "empty-name-with-primitive": { name: "", @@ -1174,9 +1264,11 @@ var nameTestCases = map[string]NameParams{ }, } +// TestName tests the `Name` function of the reflect package using various test +// cases defined in the `nameTestCases` map. func TestName(t *testing.T) { test.Map(t, nameTestCases). - Run(func(t test.Test, param NameParams) { + Run(func(t test.Test, param nameParams) { // When result := reflect.Name(param.name, param.param) diff --git a/test/README.md b/test/README.md index afdaead..2fe7d4b 100644 --- a/test/README.md +++ b/test/README.md @@ -17,8 +17,8 @@ The [`Factory`] can be instantiated by global functions with a single test parameter set ([`test.Param`][param]), a slice of test parameter sets ([`test.Slice`][slice]), or a map of test case name to test parameter sets ([`test.Map`][map] - idiomatic pattern). The tests are started by calling the -[`Run`][run], [`RunSeq`][run-seq], or [`Benchmark`][bench] methods that with -the exception of the last accept a simple test function as input, using a +[`Run`][factory], [`RunSeq`][factory], or [`Benchmark`][factory] methods that +with the exception of the last accept a simple test function as input, using a [`test.Test`][itest] interface compatible with most other extensions, e.g. [`gomock`][gomock]. @@ -28,9 +28,6 @@ the exception of the last accept a simple test function as input, using a [slice]: [map]: [itest]: -[run]: -[runseq]: -[bench]: [factory]: [context]: [filter]: @@ -43,7 +40,7 @@ test environment. ```go func TestUnit(t *testing.T) { - test.Run(test.Success, func(t test.Test){ + test.New(t).Run(func(t test.Test){ // Given mock.NewMocks(t).Expect( test.Panic("fail"), @@ -52,7 +49,7 @@ func TestUnit(t *testing.T) { // When panic("fail") ... - })(t) + }) } ``` @@ -67,7 +64,9 @@ sequential) tests using the lean test [`Factory`][factory] as follows: ```go func TestUnit(t *testing.T) { - // Set up the test using various test case definitions. + // Set up the test using various test case definitions. No need to set up + // parallel execution manually here, since this is done by default in the + // by the test factory - when possible. test.Param|Slice|Map|Any(t, unitTestCases). // Exclude of test cases temporary or permanent. Filter(test.Not(test.Pattern[T]("^test-case-prefix"))). @@ -161,72 +160,98 @@ The [`test`][test] package supports the following default filter functions: [cleanup]: -## Isolated in-test environment setup +## Manual isolated test setup -It is also possible to isolate only a single test step by setting up a small -test function that is run in isolation. +You have two options to set up a manual isolated test environment. The first +option is to create standard test functions for the default test runner using +the [`test.Run`][run] or [`test.RunSeq`][runseq] as follows: ```go func TestUnit(t *testing.T) { - test.Param|Slice|Map|Any(t, unitTestCases). - ... - // Run the test in parallel or sequential. - Run|RunSeq(func(t test.Test, param UnitParams){ + // Need to set up parallel execution manually here. + t.Parallel() + + for name, param := range unitTestCases { + t.Run(name, test.Run|test.RunSeq(func(t test.Tester) { + // Set up sequential test execution (default is test.Parallel). + t.Mode(test.Parallel|test.Sequential) + // Set up the test expectation (default is test.Success). + t.Expect(test.Success|test.Failure) + // Define a test specific timeout (default: none). + t.Timeout(50*time.Millisecond) + // Define a safety margin for cleaning up (default: none). + t.StopEarly(5*time.Millisecond) + // Given // When - test.InRun(test.Success|Failure, func(t test.Test) { - ... - })(t) // Then - }) + })) + } } ``` +To allow for more flexibility the test function is provided with an extended +[`test.Tester`][tester] interface, that allows for additional test control, +e.g. setting up the test expectation and execution mode, defining timeouts and +safety margins for cleaning up. + -## Manual isolated test environment setup +## Manual isolated test context setup -If the above pattern is not sufficient, you can create your own customized -parameterized, parallel, isolated test wrapper using the basic abstraction -`test.Run|RunSeq(test.Success|Failure, func (t test.Test) {})`: +If this pattern is insufficient, and you need more control about the test +execution, you can also create your own customized, parallel, isolated test +wrapper based on the common [`test.Context`][context]. It extends the basic +[`test.Test`][test] interface abstraction and can be utilized as follows: ```go func TestUnit(t *testing.T) { t.Parallel() - for name, param := range unitTestCases { - t.Run(name, test.Run(param.expect, func(t test.Test) { - t.Parallel() - + test.New(t). + // Set up sequential test execution (default is test.Parallel). + Mode(test.Parallel|test.Sequential). + // Set up the test expectation (default is test.Success). + Expect(test.Success|test.Failure). + // Define a test specific timeout (default: none). + Timeout(50*time.Millisecond). + // Define a safety margin for cleaning up (default: none). + StopEarly(5*time.Millisecond). + // Run the test function. + Run("test-name", func(t test.Test){ // Given // When // Then - })) - } + }) } ``` -Or finally, use even more directly the flexible `test.Context` that is -providing the features on top of the underlying `test.Test` interface -abstraction, if you need more control about the test execution: +[run]: +[runseq]: +[tester]: + + +## Isolated in-test environment setup + +Using the [before pattern](#manual-isolated-test-context-setup) it is also +possible to isolate only a single test step by setting up a small test +function that is executed in isolation: ```go func TestUnit(t *testing.T) { - t.Parallel() - - test.New(t, test.Success|Failure). - // Define a test specific timeout. - Timeout(50*time.Millisecond). - // Define a safety margin for cleaning up. - StopEarly(5*time.Millisecond). - // Run the test function. - Run("test", func(t test.Test){ + test.Param|Slice|Map|Any(t, unitTestCases). + ... + // Run the test in parallel or sequential. + Run|RunSeq(func(t test.Test, param UnitParams){ // Given // When + test.New(t).Run(func(t test.Test) { + ... + }) // Then }) @@ -237,12 +262,12 @@ func TestUnit(t *testing.T) { ## Isolated failure/panic validation Besides just capturing the failure in the isolated test environment, it is also -very simple possible to validate the failures/panics using the self installing -validator that is tightly integrated with the [`mock`](../mock) framework. +possible to easily validate failures/panics using the self installing validator +that is tightly integrated with the [`mock`](../mock) framework. ```go func TestUnit(t *testing.T) { - test.Run(func(t test.Test){ + test.New(t).Run(func(t test.Test){ // Given mock.NewMocks(t).Expect(mock.Setup( test.Errorf("fail"), diff --git a/test/benchmark_test.go b/test/benchmark_test.go index f538e2c..03baabd 100644 --- a/test/benchmark_test.go +++ b/test/benchmark_test.go @@ -13,15 +13,19 @@ import ( // TODO: check whether test validation is strong enough to detect failures. +// benchParams defines the parameters for testing the benchmark functionality. type benchParams struct { value int } +// These test cases are testing the benchmark functionality of the test +// factory. var benchTestCases = map[string]benchParams{ "case-one": {value: 1}, "case-two": {value: 2}, } +// TestBenchmarkRun is testing the benchmark functionality of the test factory. func TestBenchmarkRun(t *testing.T) { // Given var seen sync.Map @@ -42,11 +46,15 @@ func TestBenchmarkRun(t *testing.T) { assert.Equal(t, int32(len(benchTestCases)), got) } +// wrapperParams defines the parameters for testing the benchmark wrapper +// functionality. type wrapperParams struct { call func(t test.Test) any expect any } +// These test cases are testing the benchmark wrapper functionality of the test +// factory. var wrapperTestCases = map[string]wrapperParams{ // Parallel must not panic and must be a no-op. "parallel-noop": { @@ -66,6 +74,8 @@ var wrapperTestCases = map[string]wrapperParams{ }, } +// TestBenchmarkWrapper is testing the benchmark wrapper functionality of the +// test factory. func TestBenchmarkWrapper(t *testing.T) { test.Map(t, wrapperTestCases). Run(func(t test.Test, param wrapperParams) { @@ -84,12 +94,16 @@ func TestBenchmarkWrapper(t *testing.T) { }) } +// dispatchParams defines the parameters for testing the benchmark dispatch +// functionality. type dispatchParams struct { setup mock.SetupFunc factory func(t test.Test) test.Factory[benchParams] expect int32 } +// dispatchTestCases are testing the benchmark dispatch functionality of the +// test factory. var dispatchTestCases = map[string]dispatchParams{ // Map dispatches all cases. "map-all": { @@ -147,6 +161,8 @@ var dispatchTestCases = map[string]dispatchParams{ }, } +// TestBenchmarkDispatch is testing the benchmark dispatch functionality of the +// test factory. func TestBenchmarkDispatch(t *testing.T) { test.Map(t, dispatchTestCases). Run(func(t test.Test, param dispatchParams) { diff --git a/test/caller_test.go b/test/caller_test.go index 17ff0a4..55146fb 100644 --- a/test/caller_test.go +++ b/test/caller_test.go @@ -91,7 +91,7 @@ func (c *Caller) Panic(_ any) { // getCaller implements the capturing logic for the callers file and line // number for the given call. func getCaller(call func(t test.Panicer)) string { - t := test.New(&testing.T{}, false).Expect(test.Failure) + t := test.New(&testing.T{}).Mode(test.Sequential).Expect(test.Failure) mocks := mock.NewMocks(t) caller := mock.Get(mocks, func(*gomock.Controller) *Caller { diff --git a/test/common.go b/test/common.go deleted file mode 100644 index b7721c3..0000000 --- a/test/common.go +++ /dev/null @@ -1,18 +0,0 @@ -package test - -type ( - // Expect the expectation whether a test will succeed or fail. - Expect bool -) - -// Constants to express test expectations. -const ( - // Success used to express that a test is supposed to succeed. - Success Expect = true - // Failure used to express that a test is supposed to fail. - Failure Expect = false - - // Parallel is the global flag to switch test runs to be executed in - // parallel instead of sequentially. - Parallel = true -) diff --git a/test/common_test.go b/test/common_test.go deleted file mode 100644 index d637e50..0000000 --- a/test/common_test.go +++ /dev/null @@ -1,347 +0,0 @@ -package test_test - -import ( - "regexp" - "strings" - - "github.com/stretchr/testify/assert" - "github.com/tkrop/go-testing/internal/sync" - "github.com/tkrop/go-testing/mock" - "github.com/tkrop/go-testing/test" -) - -// ParamParams is a test parameter type for the test runner to test evaluation -// of default test parameter names from the test parameter set. -type ParamParams struct { - name string - expect bool -} - -// CheckName checks if the test name contains the expected name. It is used to -// verify that the test name is correctly set in the test runner. -func (p *ParamParams) CheckName(t test.Test) { - assert.Contains(t, t.Name(), - strings.ReplaceAll(p.name, " ", "-")) -} - -// TestParams is a generic test parameter type for testing the test context as -// well as the test runner using the same parameter sets. -type TestParams struct { - name string - setup mock.SetupFunc - test test.Func - expect test.Expect - consumed bool -} - -// Rename returns a new test parameter set with the given name. -func (p *TestParams) Rename(name string) TestParams { - return TestParams{ - name: name, - setup: p.setup, - test: p.test, - expect: p.expect, - consumed: p.consumed, - } -} - -// Rename returns a new test parameter set with the given name. -func (p *TestParams) Copy() TestParams { - return TestParams{ - name: p.name, - setup: p.setup, - test: p.test, - expect: p.expect, - consumed: p.consumed, - } -} - -// CheckName checks if the test name contains the expected name. -// It is used to verify that the test name is correctly set in the test runner. -func (p *TestParams) CheckName(t test.Test) { - assert.Contains(t, t.Name(), - strings.ReplaceAll(p.name, " ", "-")) -} - -// ExecTest is the generic function to execute a test with the given test -// parameters. -func (p *TestParams) ExecTest(t test.Test) { - // Given - if p.setup != nil { - mock.NewMocks(t).Expect(p.setup) - } - - wg := sync.NewLenientWaitGroup() - test.Cast[*test.Context](t).WaitGroup(wg) - if p.consumed { - wg.Add(1) - } - - // When - p.test(t) - - // Then - wg.Wait() - if p.expect == test.Failure { - assert.True(t, t.Failed()) - } -} - -// TestParamMap is a map of test parameters for testing the test context as -// well as the test runner. -type TestParamMap map[string]TestParams - -// FilterBy filters the test parameters by the given pattern to test the -// filtering of the test runner. -func (m TestParamMap) FilterBy(pattern string) TestParamMap { - filter := regexp.MustCompile(pattern) - params := TestParamMap{} - for key, value := range m { - if filter.MatchString(key) { - params[key] = value - } - } - return params -} - -// GetSlice returns the test parameters as a slice of test parameters sets. -func (m TestParamMap) GetSlice() []TestParams { - params := make([]TestParams, 0, len(m)) - for name, param := range m { - params = append(params, TestParams{ - name: name, - test: param.test, - expect: param.expect, - }) - } - return params -} - -var ( - // TestEmpty is a test function that does nothing. - TestEmpty = func(test.Test) {} - // TestSkip is a test function that skips the test. - TestSkip = func(t test.Test) { t.Skip("skip") } - // TestSkipf is a test function that skips the test with a formatted message. - TestSkipf = func(t test.Test) { t.Skipf("%s", "skip") } - // TestSkipNow is a test function that skips the test immediately. - TestSkipNow = func(t test.Test) { t.SkipNow() } - // TestLog is a test function that logs a message. - TestLog = func(t test.Test) { t.Log("log") } - // TestLogf is a test function that logs a formatted message. - TestLogf = func(t test.Test) { t.Logf("%s", "log") } - // TestError is a test function that fails with an error message. - TestError = func(t test.Test) { t.Error("fail") } - // TestErrorf is a test function that fails with a formatted error message. - TestErrorf = func(t test.Test) { t.Errorf("%s", "fail") } - // TestFatal is a test function that fails with a fatal error message. - TestFatal = func(t test.Test) { - // Duplicate terminal failures are ignored. - go func() { t.Fatal("fail") }() - t.Fatal("fail") - } - // TestFatalf is a test function that fails with a fatal formatted error - // message. - TestFatalf = func(t test.Test) { - // Duplicate terminal failures are ignored. - go func() { t.Fatalf("%s", "fail") }() - t.Fatalf("%s", "fail") - } - // TestFail is a test function that fails. - TestFail = func(t test.Test) { - // Duplicate terminal failures are ignored. - go func() { t.Fail() }() - t.Fail() - } - // TestFailNow is a test function that fails immediately. - TestFailNow = func(t test.Test) { - // Duplicate terminal failures are ignored. - go func() { t.FailNow() }() - t.FailNow() - } - // TestPanic is a test function that panics. - TestPanic = func(test.Test) { panic("fail") } - // CleanupEmpty is a cleanup function that does nothing. - CleanupEmpty = func() {} - // CleanupPanic is a cleanup function that panics. - CleanupPanic = func() { panic("cleanup") } -) - -// commonTestCases is the generic map of test parameters for testing the test -// context as well as the test runner. -var commonTestCases = TestParamMap{ - "": {}, - "base-nothing": { - test: TestEmpty, - expect: test.Success, - }, - "base-skip": { - test: TestSkip, - expect: test.Success, - }, - "base-skipf": { - test: TestSkipf, - expect: test.Success, - }, - "base-skipnow": { - test: TestSkipNow, - expect: test.Success, - }, - "base-log": { - test: TestLog, - expect: test.Success, - }, - "base-logf": { - test: TestLogf, - expect: test.Success, - }, - "base-error": { - test: TestError, - expect: test.Failure, - }, - "base-errorf": { - test: TestErrorf, - expect: test.Failure, - }, - "base-fatal": { - test: TestFatal, - expect: test.Failure, - consumed: true, - }, - "base-fatalf": { - test: TestFatalf, - expect: test.Failure, - consumed: true, - }, - "base-fail": { - test: TestFail, - expect: test.Failure, - consumed: true, - }, - "base-failnow": { - test: TestFailNow, - expect: test.Failure, - consumed: true, - }, - "base-panic": { - test: TestPanic, - expect: test.Failure, - consumed: true, - }, - - "inrun-success": { - test: test.InRun(test.Success, TestEmpty), - expect: test.Success, - }, - "inrun-success-with-skip": { - test: test.InRun(test.Success, TestSkip), - expect: test.Success, - }, - "inrun-success-with-skipf": { - test: test.InRun(test.Success, TestSkipf), - expect: test.Success, - }, - "inrun-success-with-skipnow": { - test: test.InRun(test.Success, TestSkipNow), - expect: test.Success, - }, - "inrun-success-with-log": { - test: test.InRun(test.Success, TestLog), - expect: test.Success, - }, - "inrun-success-with-logf": { - test: test.InRun(test.Success, TestLogf), - expect: test.Success, - }, - "inrun-success-with-error": { - test: test.InRun(test.Success, TestError), - expect: test.Failure, - }, - "inrun-success-with-errorf": { - test: test.InRun(test.Success, TestErrorf), - expect: test.Failure, - }, - "inrun-success-with-fatal": { - test: test.InRun(test.Success, TestFatal), - expect: test.Failure, - consumed: true, - }, - "inrun-success-with-fatalf": { - test: test.InRun(test.Success, TestFatalf), - expect: test.Failure, - consumed: true, - }, - "inrun-success-with-fail": { - test: test.InRun(test.Success, TestFail), - expect: test.Failure, - consumed: true, - }, - "inrun-success-with-failnow": { - test: test.InRun(test.Success, TestFailNow), - expect: test.Failure, - consumed: true, - }, - "inrun-success-with-panic": { - test: test.InRun(test.Success, TestPanic), - expect: test.Failure, - consumed: true, - }, - - "inrun-failure": { - test: test.InRun(test.Failure, TestEmpty), - expect: test.Failure, - }, - "inrun-failure-with-skip": { - test: test.InRun(test.Failure, TestSkip), - expect: test.Failure, - }, - "inrun-failure-with-skipf": { - test: test.InRun(test.Failure, TestSkipf), - expect: test.Failure, - }, - "inrun-failure-with-skipnow": { - test: test.InRun(test.Failure, TestSkipNow), - expect: test.Failure, - }, - "inrun-failure-with-log": { - test: test.InRun(test.Failure, TestLog), - expect: test.Failure, - }, - "inrun-failure-with-logf": { - test: test.InRun(test.Failure, TestLogf), - expect: test.Failure, - }, - "inrun-failure-with-error": { - test: test.InRun(test.Failure, TestError), - expect: test.Success, - }, - "inrun-failure-with-errorf": { - test: test.InRun(test.Failure, TestErrorf), - expect: test.Success, - }, - "inrun-failure-with-fatal": { - test: test.InRun(test.Failure, TestFatal), - expect: test.Success, - consumed: true, - }, - "inrun-failure-with-fatalf": { - test: test.InRun(test.Failure, TestFatalf), - expect: test.Success, - consumed: true, - }, - "inrun-failure-with-fail": { - test: test.InRun(test.Failure, TestFail), - expect: test.Success, - consumed: true, - }, - "inrun-failure-with-failnow": { - test: test.InRun(test.Failure, TestFailNow), - expect: test.Success, - consumed: true, - }, - "inrun-failure-with-panic": { - test: test.InRun(test.Failure, TestPanic), - expect: test.Success, - consumed: true, - }, -} diff --git a/test/context.go b/test/context.go index 297f88d..89f3bab 100644 --- a/test/context.go +++ b/test/context.go @@ -14,6 +14,31 @@ import ( "github.com/tkrop/go-testing/internal/sync" ) +// Types and constants for the test context and test runner. +type ( + // Expect the expectation whether a test will succeed or fail. + Expect bool + // Mode defines the test execution mode. Currently mode only supports + // either parallel or sequential. + Mode int +) + +// Constants to express test expectations. +const ( + // Success used to express that a test is supposed to succeed. + Success Expect = true + // Failure used to express that a test is supposed to fail. + Failure Expect = false +) + +// Constants to express test execution modes. +const ( + // Sequential is the test execution mode to run tests in sequence. + Sequential Mode = 0x0 + // Parallel is the test execution mode to run tests in parallel. + Parallel Mode = 0x1 +) + // Test is a minimal interface for abstracting test methods that are needed to // setup an isolated test environment for GoMock and Testify. type Test interface { //nolint:interfacebloat // Minimal interface. @@ -47,6 +72,48 @@ type Test interface { //nolint:interfacebloat // Minimal interface. Cleanup(cleanup func()) } +type Tester interface { + // Embeds the minimal test interface. + Test + // Embeds the test failure reporter interface. + Panicer + // Mode sets up the test execution mode, either `Parallel` or `Sequential`. + // + // **Note:** `Parallel` only affects the current test context before the + // test execution is started, i.e. before `Run` is called. After the test + // execution is started, calling this method only affects sub-tests. + Mode(mode Mode) Tester + // Expect sets up a different expected test outcome, i.e. `test.Success` or + // `test.Failure`. Can be called multiple times, but the last call wins. + Expect(expect Expect) Tester + // Timeout sets up an individual timeout for the test. The method does not + // affect the global test timeout or a pending parent timeout that may abort + // the test, if the given duration is exceeding the timeout. + // + // A negative or zero duration is ignored and will not change the timeout. If + // this method is called multiple times, the last call wins. + Timeout(timeout time.Duration) Tester + // StopEarly stops the test by the given duration ahead of the individual or + // global test deadline, to ensure that a cleanup function has sufficient time + // to finish before the deadline is exceeded. The method is not able to extend + // the test deadline. + // + // A negative or zero duration is ignored. **Warning:** calling this method + // multiple times will also reduce the test time step by step. + StopEarly(time time.Duration) Tester + // WaitGroup adds wait group to unlock in case of a failure. + // + //revive:disable-next-line:waitgroup-by-value // own wrapper interface + WaitGroup(wg sync.WaitGroup) + // Reporter sets up a test failure reporter. This can be used to validate the + // reported failures in a test environment. + Reporter(reporter Reporter) + // Run executes the test function in a safe detached environment and checks + // the failure state after the test function has finished. If the expectation + // is not met, a failure is created in the parent test context. + Run(name string, call Func) +} + // Cleanuper defines an interface to add a custom method that is called after // the test execution to cleanup the test environment. type Cleanuper interface { @@ -57,42 +124,57 @@ type Cleanuper interface { type Func func(Test) // Run creates an isolated (by default) parallel test context running the given -// test function with given expectation. If the expectation is not met, a test +// test function with success expectation. If the expectation is not met, a test // failure is created in the parent test context. // -// **Note:** even though the test context is created with parallelization, the -// test is still able to call `t.Parallel()`, since the context is swallowing -// the panic that is raised when calling `t.Parallel()` multiple times. -func Run(expect Expect, test Func) func(*testing.T) { +// **Note:** You can still call `Expect(Failure)` to change the expected test +// outcome. You may also call `Parallel()`, since the context is swallowing the +// panic that is raised when calling `Parallel()` multiple times. +func Run(test func(Tester)) func(*testing.T) { return func(t *testing.T) { t.Helper() - New(t, Parallel).Expect(expect).Run("", test) + New(t).Run("", func(t Test) { + t.Helper() + + test(Cast[Tester](t)) + }) } } // RunSeq creates an isolated, test context for the given test function with -// given expectation. If the expectation is not met, a test failure is created -// in the parent test context. +// default success expectation. If the expectation is not met, a test failure +// is created in the parent test context. // -// **Note:** even though the test context is created with out parallelization, -// you can still setup tests using `t.Parallel()` manually. -func RunSeq(expect Expect, test Func) func(*testing.T) { +// **Note:** You can still call `Expect(Failure)` to change the expected test +// outcome. You may also call `Parallel()` to parallelize the test execution. +// Repeated calls to `Parallel()` are also allowed, since the context is +// swallowing the raised panic. +func RunSeq(test func(Tester)) func(*testing.T) { return func(t *testing.T) { t.Helper() - New(t, !Parallel).Expect(expect).Run("", test) + New(t).Mode(Sequential).Run("", func(t Test) { + t.Helper() + + test(Cast[Tester](t)) + }) } } -// InRun creates an isolated, (by default) sequential test context for the -// given test function with given expectation. If the expectation is not met, a -// test failure is created in the parent test context. -func InRun(expect Expect, test Func) Func { +// Wrap wraps a test function into an isolated, sequential test context with +// given test expectation. If the expectation is not met, the wrapper creates a +// failure in the parent test context. +// +// **Note:** You can call `Parallel()` to parallelize the test execution. +// If you cast the basic `Test` interface to a `Tester`, you may also call +// `Expect(Success|Failure)`, `Timeout()`, and `StopEarly()` to change the +// test behavior and expectations. +func Wrap(expect Expect, test Func) Func { return func(t Test) { t.Helper() - New(t, !Parallel).Expect(expect).Run("", test) + New(t).Mode(Sequential).Expect(expect).Run("", test) } } @@ -109,19 +191,31 @@ type Context struct { reporter Reporter cleanups []func() expect Expect - parallel bool + mode Mode } -// New creates a new minimal isolated test context based on the given test -// context with. The parent test context is used to delegate methods calls -// to the parent context to propagate test results. -func New(t Test, parallel bool) *Context { - if tx, ok := t.(*Context); ok { +// New creates a new minimal isolated test context (`Tester`) based on the +// provided test context, executed in a safe detached, parallel environment +// expecting a successful test execution. The test context is delegating all +// method calls to the parent test context, which is used to propagate test +// results. +// +// If the provided test context is already of type `*Context`, the new context +// is created based on the existing state to simplify consistent nested context +// creation with same deadline, same wait group, and same expectations. +// +// **Note:** even though the test context is created with parallelization and +// successful execution expectation, this expectation can be changed by calling +// `Mode(Sequential)` or `Expect(Failure)` to change the behavior. The test +// function can also simply call `Parallel()`, since the context is silently +// swallowing the panic raised when calling `Parallel()` multiple times. +func New(t Test) Tester { + if c, ok := t.(*Context); ok { return &Context{ - t: tx, wg: tx.wg, - deadline: tx.deadline, - expect: true, - parallel: parallel, + t: c, wg: c.wg, + deadline: c.deadline, + expect: c.expect, + mode: c.mode, } } @@ -132,22 +226,38 @@ func New(t Test, parallel bool) *Context { deadline, _ := t.Deadline() return deadline }(t), - expect: true, - parallel: parallel, + expect: true, + mode: Parallel, } } +// Mode sets up the test execution mode, either `Parallel` or `Sequential`. +// +// **Note:** `Parallel` only affects the current test context before the test +// execution is started, i.e. before `Run` is called. After the test execution +// is started, calling this method only affects sub-tests. +func (c *Context) Mode(mode Mode) Tester { + c.t.Helper() + + c.mu.Lock() + defer c.mu.Unlock() + + c.mode = mode + + return c +} + // Expect sets up a different expected test outcome, i.e. `test.Success` or // `test.Failure`. Can be called multiple times, but the last call wins. -func (t *Context) Expect(expect Expect) *Context { - t.t.Helper() +func (c *Context) Expect(expect Expect) Tester { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - t.expect = expect + c.expect = expect - return t + return c } // Timeout sets up an individual timeout for the test. The method does not @@ -156,17 +266,17 @@ func (t *Context) Expect(expect Expect) *Context { // // A negative or zero duration is ignored and will not change the timeout. If // this method is called multiple times, the last call wins. -func (t *Context) Timeout(timeout time.Duration) *Context { - t.t.Helper() +func (c *Context) Timeout(timeout time.Duration) Tester { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() if timeout > 0 { - t.deadline = time.Now().Add(timeout) + c.deadline = time.Now().Add(timeout) } - return t + return c } // StopEarly stops the test by the given duration ahead of the individual or @@ -176,209 +286,209 @@ func (t *Context) Timeout(timeout time.Duration) *Context { // // A negative or zero duration is ignored. **Warning:** calling this method // multiple times will also reduce the test time step by step. -func (t *Context) StopEarly(time time.Duration) *Context { - t.t.Helper() +func (c *Context) StopEarly(time time.Duration) Tester { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - if !t.deadline.IsZero() && time > 0 { - t.deadline = t.deadline.Add(-time) + if !c.deadline.IsZero() && time > 0 { + c.deadline = c.deadline.Add(-time) } - return t + return c } // WaitGroup adds wait group to unlock in case of a failure. // //revive:disable-next-line:waitgroup-by-value // own wrapper interface -func (t *Context) WaitGroup(wg sync.WaitGroup) { - t.t.Helper() +func (c *Context) WaitGroup(wg sync.WaitGroup) { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - t.wg = wg + c.wg = wg } // Done decrements the wait group of the test by one. -func (t *Context) Done() { - t.t.Helper() +func (c *Context) Done() { + c.t.Helper() - if t.wg != nil { - t.wg.Done() + if c.wg != nil { + c.wg.Done() } } // Reporter sets up a test failure reporter. This can be used to validate the // reported failures in a test environment. -func (t *Context) Reporter(reporter Reporter) { - t.t.Helper() +func (c *Context) Reporter(reporter Reporter) { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - t.reporter = reporter + c.reporter = reporter } // Cleanup is a function called to setup test cleanup after execution. This // method is allowing `gomock` to register its `finish` method that reports the // missing mock calls. -func (t *Context) Cleanup(cleanup func()) { - t.t.Helper() +func (c *Context) Cleanup(cleanup func()) { + c.t.Helper() if cleanup == nil { return } - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - t.cleanups = append(t.cleanups, cleanup) + c.cleanups = append(c.cleanups, cleanup) } // Name delegates the request to the parent test context. -func (t *Context) Name() string { - t.t.Helper() +func (c *Context) Name() string { + c.t.Helper() - return t.t.Name() + return c.t.Name() } // Helper delegates request to the parent test context. -func (t *Context) Helper() { - t.t.Helper() +func (c *Context) Helper() { + c.t.Helper() } // Parallel robustly delegates request to the parent context. It can be called // multiple times, since it is swallowing the panic that is raised when calling // `t.Parallel()` multiple times. -func (t *Context) Parallel() { - t.t.Helper() +func (c *Context) Parallel() { + c.t.Helper() defer func() { if err := recover(); err != nil && err != "testing: t.Parallel called multiple times" { - t.Panic(err) + c.Panic(err) } }() - t.t.Parallel() + c.t.Parallel() } // TempDir delegates the request to the parent test context. -func (t *Context) TempDir() string { - t.t.Helper() - return t.t.TempDir() +func (c *Context) TempDir() string { + c.t.Helper() + return c.t.TempDir() } // Setenv delegates request to the parent context, if it is of type // `*testing.T`. Else it is swallowing the request silently. -func (t *Context) Setenv(key, value string) { - t.t.Helper() +func (c *Context) Setenv(key, value string) { + c.t.Helper() - t.t.Setenv(key, value) + c.t.Setenv(key, value) } // Deadline delegates request to the parent context. It returns the deadline of // the test and a flag indicating whether the deadline is set. -func (t *Context) Deadline() (time.Time, bool) { - t.t.Helper() +func (c *Context) Deadline() (time.Time, bool) { + c.t.Helper() - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - if !t.deadline.IsZero() { - return t.deadline, true + if !c.deadline.IsZero() { + return c.deadline, true } - return t.t.Deadline() + return c.t.Deadline() } // Skip delegates request to the parent context. It is a helper method to skip // the test. -func (t *Context) Skip(args ...any) { - t.t.Helper() +func (c *Context) Skip(args ...any) { + c.t.Helper() - t.t.Skip(args...) + c.t.Skip(args...) } // Skipf delegates request to the parent context. It is a helper method to skip // the test with a formatted message. -func (t *Context) Skipf(format string, args ...any) { - t.t.Helper() +func (c *Context) Skipf(format string, args ...any) { + c.t.Helper() - t.t.Skipf(format, args...) + c.t.Skipf(format, args...) } // SkipNow delegates request to the parent context. It is a helper method to skip // the test immediately. -func (t *Context) SkipNow() { - t.t.Helper() +func (c *Context) SkipNow() { + c.t.Helper() - t.t.SkipNow() + c.t.SkipNow() } // Skipped delegates request to the parent context. It reports whether the test // has been skipped. -func (t *Context) Skipped() bool { - t.t.Helper() +func (c *Context) Skipped() bool { + c.t.Helper() - return t.t.Skipped() + return c.t.Skipped() } // Log delegates request to the parent context. It provides a logging function // for the test. -func (t *Context) Log(args ...any) { - t.t.Helper() +func (c *Context) Log(args ...any) { + c.t.Helper() - t.t.Log(args...) - if t.reporter != nil { - t.reporter.Log(args...) + c.t.Log(args...) + if c.reporter != nil { + c.reporter.Log(args...) } } // Logf delegates request to the parent context. It provides a logging function // for the test. -func (t *Context) Logf(format string, args ...any) { - t.t.Helper() +func (c *Context) Logf(format string, args ...any) { + c.t.Helper() - t.t.Logf(format, args...) - if t.reporter != nil { - t.reporter.Logf(format, args...) + c.t.Logf(format, args...) + if c.reporter != nil { + c.reporter.Logf(format, args...) } } // Error handles failure messages where the test is supposed to continue. On // an expected success, the failure is also delegated to the parent test // context. Else it delegates the request to the test reporter if available. -func (t *Context) Error(args ...any) { - t.t.Helper() +func (c *Context) Error(args ...any) { + c.t.Helper() - t.failed.Store(true) + c.failed.Store(true) - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - if t.expect == Success { - t.t.Error(args...) - } else if t.reporter != nil { - t.reporter.Error(args...) + if c.expect == Success { + c.t.Error(args...) + } else if c.reporter != nil { + c.reporter.Error(args...) } } // Errorf handles failure messages where the test is supposed to continue. On // an expected success, the failure is also delegated to the parent test // context. Else it delegates the request to the test reporter if available. -func (t *Context) Errorf(format string, args ...any) { - t.t.Helper() +func (c *Context) Errorf(format string, args ...any) { + c.t.Helper() - t.failed.Store(true) + c.failed.Store(true) - t.mu.Lock() - defer t.mu.Unlock() + c.mu.Lock() + defer c.mu.Unlock() - if t.expect == Success { - t.t.Errorf(format, args...) - } else if t.reporter != nil { - t.reporter.Errorf(format, args...) + if c.expect == Success { + c.t.Errorf(format, args...) + } else if c.reporter != nil { + c.reporter.Errorf(format, args...) } } @@ -386,16 +496,16 @@ func (t *Context) Errorf(format string, args ...any) { // execution. On an expected success, the failure handling is also delegated // to the parent test context. Else it delegates the request to the test // reporter if available. -func (t *Context) Fatal(args ...any) { - t.t.Helper() +func (c *Context) Fatal(args ...any) { + c.t.Helper() - t.lockOrExit() - defer t.unlock() + c.lockOrExit() + defer c.unlock() - if t.expect == Success { - t.t.Fatal(args...) - } else if t.reporter != nil { - t.reporter.Fatal(args...) + if c.expect == Success { + c.t.Fatal(args...) + } else if c.reporter != nil { + c.reporter.Fatal(args...) } runtime.Goexit() } @@ -404,16 +514,16 @@ func (t *Context) Fatal(args ...any) { // execution. On an expected success, the failure handling is also delegated // to the parent test context. Else it delegates the request to the test // reporter if available. -func (t *Context) Fatalf(format string, args ...any) { - t.t.Helper() +func (c *Context) Fatalf(format string, args ...any) { + c.t.Helper() - t.lockOrExit() - defer t.unlock() + c.lockOrExit() + defer c.unlock() - if t.expect == Success { - t.t.Fatalf(format, args...) - } else if t.reporter != nil { - t.reporter.Fatalf(format, args...) + if c.expect == Success { + c.t.Fatalf(format, args...) + } else if c.reporter != nil { + c.reporter.Fatalf(format, args...) } runtime.Goexit() } @@ -421,16 +531,16 @@ func (t *Context) Fatalf(format string, args ...any) { // Fail handles a failure message that immediate aborts of the test execution. // On an expected success, the failure handling is also delegated to the parent // test context. Else it delegates the request to the test reporter if available. -func (t *Context) Fail() { - t.t.Helper() +func (c *Context) Fail() { + c.t.Helper() - t.lockOrExit() - defer t.unlock() + c.lockOrExit() + defer c.unlock() - if t.expect == Success { - t.t.Fail() - } else if t.reporter != nil { - t.reporter.Fail() + if c.expect == Success { + c.t.Fail() + } else if c.reporter != nil { + c.reporter.Fail() } runtime.Goexit() } @@ -439,25 +549,25 @@ func (t *Context) Fail() { // test execution immediately. On an expected success, it the failure handling // is also delegated to the parent test context. Else it delegates the request // to the test reporter if available. -func (t *Context) FailNow() { - t.t.Helper() +func (c *Context) FailNow() { + c.t.Helper() - t.lockOrExit() - defer t.unlock() + c.lockOrExit() + defer c.unlock() - if t.expect == Success { - t.t.FailNow() - } else if t.reporter != nil { - t.reporter.FailNow() + if c.expect == Success { + c.t.FailNow() + } else if c.reporter != nil { + c.reporter.FailNow() } runtime.Goexit() } // Failed reports whether the test has failed. -func (t *Context) Failed() bool { - t.t.Helper() +func (c *Context) Failed() bool { + c.t.Helper() - return t.failed.Load() + return c.failed.Load() } // regexPanic is a regular expression to extract the actual important panic @@ -467,17 +577,17 @@ var regexPanic = regexp.MustCompile(`(?m)\nruntime\/debug\.Stack\(\)` + // Panic handles failure notifications of panics that also abort the test // execution immediately. -func (t *Context) Panic(arg any) { - t.t.Helper() +func (c *Context) Panic(arg any) { + c.t.Helper() - t.lockOrExit() - defer t.unlock() + c.lockOrExit() + defer c.unlock() - if t.expect == Success { + if c.expect == Success { stack := regexPanic.Split(string(debug.Stack()), -1) - t.t.Fatalf("panic: %v\n%s\n%s", arg, stack[0], stack[1]) - } else if t.reporter != nil { - if reporter, ok := t.reporter.(Panicer); ok { + c.t.Fatalf("panic: %v\n%s\n%s", arg, stack[0], stack[1]) + } else if c.reporter != nil { + if reporter, ok := c.reporter.(Panicer); ok { reporter.Panic(arg) } } @@ -491,37 +601,37 @@ func (t *Context) Panic(arg any) { // If name is non-empty, a named sub-test is created by delegating to the // underlying test runner, allowing *Context to be used wherever a named // sub-test is created using reflect.Run. -func (t *Context) Run(name string, call Func) { - t.t.Helper() +func (c *Context) Run(name string, call Func) { + c.t.Helper() if name != "" { - reflect.Run(t.t, name, call) + reflect.Run(c.t, name, call) return } - if t.parallel { - t.t.Parallel() + if c.mode == Parallel { + c.t.Parallel() } // Register cleanup handlers. - t.register() + c.register() // Setup shorter deadline for detached test function. wait := time.Duration(math.MaxInt64) - if deadline, ok := t.Deadline(); ok { + if deadline, ok := c.Deadline(); ok { wait = time.Until(deadline) } // Execute test function with channel to signal completion. done := make(chan any, 1) - go t.run(call, done) + go c.run(call, done) // Wait for test to finish or deadline to expire. select { case <-done: // Panic is already handled by the reporter. case <-time.After(wait): - t.Fatal("stopped by deadline") + c.Fatal("stopped by deadline") } } @@ -530,84 +640,84 @@ func (t *Context) Run(name string, call Func) { // the waiting test context. // // The function is supposed to be called in a goroutine. -func (t *Context) run(test Func, done chan any) { - t.t.Helper() +func (c *Context) run(test Func, done chan any) { + c.t.Helper() defer func() { - t.t.Helper() + c.t.Helper() // Unlock the waiting test context. defer func() { done <- nil }() // Intercept and report panic as a failure. if arg := recover(); arg != nil { - t.Panic(arg) + c.Panic(arg) } }() - test(t) + test(c) } // register registers the clean up handlers with the parent test context. -func (t *Context) register() { - t.t.Helper() +func (c *Context) register() { + c.t.Helper() // Register cleanup handlers with the parent test context. - if c, ok := t.t.(Cleanuper); ok { - c.Cleanup(func() { - t.t.Helper() + if cu, ok := c.t.(Cleanuper); ok { + cu.Cleanup(func() { + c.t.Helper() - for i := len(t.cleanups) - 1; i >= 0; i-- { - t.cleanups[i]() + for i := len(c.cleanups) - 1; i >= 0; i-- { + c.cleanups[i]() } }) } // Register handler to unlocked the waiting test context. - t.Cleanup(func() { - t.t.Helper() - t.finish() + c.Cleanup(func() { + c.t.Helper() + c.finish() }) } // finish evaluates the final result of the test function in relation to the // provided expectation. -func (t *Context) finish() { - t.mu.Lock() - defer t.mu.Unlock() +func (c *Context) finish() { + c.mu.Lock() + defer c.mu.Unlock() - if t.t.Skipped() { + if c.t.Skipped() { return } - switch t.expect { + switch c.expect { case Success: - if t.failed.Load() { - t.t.Errorf("Expected test to succeed but it failed: %s", t.Name()) + if c.failed.Load() { + c.t.Errorf("Expected test to succeed but it failed: %s", c.Name()) } case Failure: - if !t.failed.Load() { - t.t.Errorf("Expected test to fail but it succeeded: %s", t.Name()) + if !c.failed.Load() { + c.t.Errorf("Expected test to fail but it succeeded: %s", c.Name()) } } } // lockOrExit either locks the test mutex or aborts a test in case of a pending // test failure to ensure that only the first failure is reported. -func (t *Context) lockOrExit() { - t.t.Helper() +func (c *Context) lockOrExit() { + c.t.Helper() - if t.expect == Failure && t.failed.Swap(true) { + if c.expect == Failure && c.failed.Swap(true) { runtime.Goexit() } - t.mu.Lock() + c.mu.Lock() } // unlock unlocks the wait group of the test by consuming the wait group // counter completely. -func (t *Context) unlock() { - if t.wg != nil { - t.wg.Add(math.MinInt) +func (c *Context) unlock() { + if c.wg != nil { + c.wg.Add(math.MinInt) } - t.mu.Unlock() + c.mu.Unlock() } diff --git a/test/context_test.go b/test/context_test.go index db350e3..2285148 100644 --- a/test/context_test.go +++ b/test/context_test.go @@ -2,6 +2,8 @@ package test_test import ( "os" + "regexp" + "strings" "testing" "time" @@ -10,16 +12,357 @@ import ( "github.com/tkrop/go-testing/internal/sync" "github.com/tkrop/go-testing/mock" + "github.com/tkrop/go-testing/reflect" "github.com/tkrop/go-testing/test" ) +// paramParams is a test parameter type for the test runner to test evaluation +// of default test parameter names from the test parameter set. +type paramParams struct { + name string + expect bool +} + +// CheckName checks if the test name contains the expected name. It is used to +// verify that the test name is correctly set in the test runner. +func (p *paramParams) CheckName(t test.Test) { + assert.Contains(t, t.Name(), + strings.ReplaceAll(p.name, " ", "-")) +} + +// testParams is a generic test parameter type for testing the test context as +// well as the test runner using the same parameter sets. +type testParams struct { + name string + setup mock.SetupFunc + test test.Func + expect test.Expect + consumed bool +} + +// Rename returns a new test parameter set with the given name. +func (p *testParams) Rename(name string) testParams { + return testParams{ + name: name, + setup: p.setup, + test: p.test, + expect: p.expect, + consumed: p.consumed, + } +} + +// Rename returns a new test parameter set with the given name. +func (p *testParams) Copy() testParams { + return testParams{ + name: p.name, + setup: p.setup, + test: p.test, + expect: p.expect, + consumed: p.consumed, + } +} + +// CheckName checks if the test name contains the expected name. +// It is used to verify that the test name is correctly set in the test runner. +func (p *testParams) CheckName(t test.Test) { + assert.Contains(t, t.Name(), + strings.ReplaceAll(p.name, " ", "-")) +} + +// ExecTest is the generic function to execute a test with the given test +// parameters. +func (p *testParams) ExecTest(t test.Test) { + // Given + if p.setup != nil { + mock.NewMocks(t).Expect(p.setup) + } + + wg := sync.NewLenientWaitGroup() + test.Cast[*test.Context](t).WaitGroup(wg) + if p.consumed { + wg.Add(1) + } + + // When + p.test(t) + + // Then + wg.Wait() + if p.expect == test.Failure { + assert.True(t, t.Failed()) + } +} + +// testParamMap is a map of test parameters for testing the test context as +// well as the test runner. +type testParamMap map[string]testParams + +// FilterBy filters the test parameters by the given pattern to test the +// filtering of the test runner. +func (m testParamMap) FilterBy(pattern string) testParamMap { + filter := regexp.MustCompile(pattern) + params := testParamMap{} + for key, value := range m { + if filter.MatchString(key) { + params[key] = value + } + } + return params +} + +// GetSlice returns the test parameters as a slice of test parameters sets. +func (m testParamMap) GetSlice() []testParams { + params := make([]testParams, 0, len(m)) + for name, param := range m { + params = append(params, testParams{ + name: name, + test: param.test, + expect: param.expect, + }) + } + return params +} + +var ( + // TestEmpty is a test function that does nothing. + TestEmpty = func(test.Test) {} + // TestSkip is a test function that skips the test. + TestSkip = func(t test.Test) { t.Skip("skip") } + // TestSkipf is a test function that skips the test with a formatted message. + TestSkipf = func(t test.Test) { t.Skipf("%s", "skip") } + // TestSkipNow is a test function that skips the test immediately. + TestSkipNow = func(t test.Test) { t.SkipNow() } + // TestLog is a test function that logs a message. + TestLog = func(t test.Test) { t.Log("log") } + // TestLogf is a test function that logs a formatted message. + TestLogf = func(t test.Test) { t.Logf("%s", "log") } + // TestError is a test function that fails with an error message. + TestError = func(t test.Test) { t.Error("fail") } + // TestErrorf is a test function that fails with a formatted error message. + TestErrorf = func(t test.Test) { t.Errorf("%s", "fail") } + // TestFatal is a test function that fails with a fatal error message. + TestFatal = func(t test.Test) { + // Duplicate terminal failures are ignored. + go func() { t.Fatal("fail") }() + t.Fatal("fail") + } + // TestFatalf is a test function that fails with a fatal formatted error + // message. + TestFatalf = func(t test.Test) { + // Duplicate terminal failures are ignored. + go func() { t.Fatalf("%s", "fail") }() + t.Fatalf("%s", "fail") + } + // TestFail is a test function that fails. + TestFail = func(t test.Test) { + // Duplicate terminal failures are ignored. + go func() { t.Fail() }() + t.Fail() + } + // TestFailNow is a test function that fails immediately. + TestFailNow = func(t test.Test) { + // Duplicate terminal failures are ignored. + go func() { t.FailNow() }() + t.FailNow() + } + // TestPanic is a test function that panics. + TestPanic = func(test.Test) { panic("fail") } + // CleanupEmpty is a cleanup function that does nothing. + CleanupEmpty = func() {} + // CleanupPanic is a cleanup function that panics. + CleanupPanic = func() { panic("cleanup") } +) + +// commonTestCases is the generic map of test parameters for testing the test +// context as well as the test runner. +var commonTestCases = testParamMap{ + "": {}, + "base-nothing": { + test: TestEmpty, + expect: test.Success, + }, + "base-skip": { + test: TestSkip, + expect: test.Success, + }, + "base-skipf": { + test: TestSkipf, + expect: test.Success, + }, + "base-skipnow": { + test: TestSkipNow, + expect: test.Success, + }, + "base-log": { + test: TestLog, + expect: test.Success, + }, + "base-logf": { + test: TestLogf, + expect: test.Success, + }, + "base-error": { + test: TestError, + expect: test.Failure, + }, + "base-errorf": { + test: TestErrorf, + expect: test.Failure, + }, + "base-fatal": { + test: TestFatal, + expect: test.Failure, + consumed: true, + }, + "base-fatalf": { + test: TestFatalf, + expect: test.Failure, + consumed: true, + }, + "base-fail": { + test: TestFail, + expect: test.Failure, + consumed: true, + }, + "base-failnow": { + test: TestFailNow, + expect: test.Failure, + consumed: true, + }, + "base-panic": { + test: TestPanic, + expect: test.Failure, + consumed: true, + }, + + "inrun-success": { + test: test.Wrap(test.Success, TestEmpty), + expect: test.Success, + }, + "inrun-success-with-skip": { + test: test.Wrap(test.Success, TestSkip), + expect: test.Success, + }, + "inrun-success-with-skipf": { + test: test.Wrap(test.Success, TestSkipf), + expect: test.Success, + }, + "inrun-success-with-skipnow": { + test: test.Wrap(test.Success, TestSkipNow), + expect: test.Success, + }, + "inrun-success-with-log": { + test: test.Wrap(test.Success, TestLog), + expect: test.Success, + }, + "inrun-success-with-logf": { + test: test.Wrap(test.Success, TestLogf), + expect: test.Success, + }, + "inrun-success-with-error": { + test: test.Wrap(test.Success, TestError), + expect: test.Failure, + }, + "inrun-success-with-errorf": { + test: test.Wrap(test.Success, TestErrorf), + expect: test.Failure, + }, + "inrun-success-with-fatal": { + test: test.Wrap(test.Success, TestFatal), + expect: test.Failure, + consumed: true, + }, + "inrun-success-with-fatalf": { + test: test.Wrap(test.Success, TestFatalf), + expect: test.Failure, + consumed: true, + }, + "inrun-success-with-fail": { + test: test.Wrap(test.Success, TestFail), + expect: test.Failure, + consumed: true, + }, + "inrun-success-with-failnow": { + test: test.Wrap(test.Success, TestFailNow), + expect: test.Failure, + consumed: true, + }, + "inrun-success-with-panic": { + test: test.Wrap(test.Success, TestPanic), + expect: test.Failure, + consumed: true, + }, + + "inrun-failure": { + test: test.Wrap(test.Failure, TestEmpty), + expect: test.Failure, + }, + "inrun-failure-with-skip": { + test: test.Wrap(test.Failure, TestSkip), + expect: test.Failure, + }, + "inrun-failure-with-skipf": { + test: test.Wrap(test.Failure, TestSkipf), + expect: test.Failure, + }, + "inrun-failure-with-skipnow": { + test: test.Wrap(test.Failure, TestSkipNow), + expect: test.Failure, + }, + "inrun-failure-with-log": { + test: test.Wrap(test.Failure, TestLog), + expect: test.Failure, + }, + "inrun-failure-with-logf": { + test: test.Wrap(test.Failure, TestLogf), + expect: test.Failure, + }, + "inrun-failure-with-error": { + test: test.Wrap(test.Failure, TestError), + expect: test.Success, + }, + "inrun-failure-with-errorf": { + test: test.Wrap(test.Failure, TestErrorf), + expect: test.Success, + }, + "inrun-failure-with-fatal": { + test: test.Wrap(test.Failure, TestFatal), + expect: test.Success, + consumed: true, + }, + "inrun-failure-with-fatalf": { + test: test.Wrap(test.Failure, TestFatalf), + expect: test.Success, + consumed: true, + }, + "inrun-failure-with-fail": { + test: test.Wrap(test.Failure, TestFail), + expect: test.Success, + consumed: true, + }, + "inrun-failure-with-failnow": { + test: test.Wrap(test.Failure, TestFailNow), + expect: test.Success, + consumed: true, + }, + "inrun-failure-with-panic": { + test: test.Wrap(test.Failure, TestPanic), + expect: test.Success, + consumed: true, + }, +} + // TestRun is testing the test context with single test cases running in // parallel. func TestRun(t *testing.T) { t.Parallel() for name, param := range commonTestCases { - t.Run(name, test.Run(param.expect, func(t test.Test) { + t.Run(name, test.Run(func(t test.Tester) { + // Given + t.Expect(param.expect) + + // Then param.CheckName(t) param.ExecTest(t) })) @@ -27,14 +370,18 @@ func TestRun(t *testing.T) { } // TestRunSeq is testing the test context with single test cases running in -// sequence. +// sequence - but still running in parallel. func TestRunSeq(t *testing.T) { t.Parallel() for name, param := range commonTestCases { - t.Run(name, test.RunSeq(param.expect, func(t test.Test) { + t.Run(name, test.RunSeq(func(t test.Tester) { t.Parallel() + // Given + t.Expect(param.expect) + + // Then param.CheckName(t) param.ExecTest(t) })) @@ -45,19 +392,20 @@ func TestRunSeq(t *testing.T) { func TestTempDir(t *testing.T) { t.Parallel() - t.Run("create", test.Run(test.Success, func(t test.Test) { + t.Run("create", test.Run(func(t test.Tester) { + // Then assert.NotEmpty(t, t.TempDir()) })) } -// ContextParams is a test parameter type for testing the test context. -type ContextParams struct { +// contextParams is a test parameter type for testing the test context. +type contextParams struct { setup mock.SetupFunc test test.Func } // contextTestCases is a map of test parameters for testing the test context. -var contextTestCases = map[string]ContextParams{ +var contextTestCases = map[string]contextParams{ "panic": { setup: mock.Chain( test.Fatalf("panic: %v\n%s\n%s", "test", gomock.Any(), gomock.Any()), @@ -66,31 +414,74 @@ var contextTestCases = map[string]ContextParams{ panic("test") }, }, - // TODO: add more test cases for the test context. + + // Sub-context copy-semantics cases. + "success-sequential": { + test: func(t test.Test) { + parent := test.New(t).Mode(test.Sequential).Expect(test.Success) + child := test.New(parent) + getter := reflect.NewGetter(child) + + assert.Equal(t, test.Success, getter.Get("expect")) + assert.Equal(t, test.Sequential, getter.Get("mode")) + }, + }, + "success-parallel": { + test: func(t test.Test) { + parent := test.New(t).Mode(test.Parallel).Expect(test.Success) + child := test.New(parent) + getter := reflect.NewGetter(child) + + assert.Equal(t, test.Success, getter.Get("expect")) + assert.Equal(t, test.Parallel, getter.Get("mode")) + }, + }, + "failure-sequential": { + test: func(t test.Test) { + parent := test.New(t).Mode(test.Sequential).Expect(test.Failure) + child := test.New(parent) + getter := reflect.NewGetter(child) + + assert.Equal(t, test.Failure, getter.Get("expect")) + assert.Equal(t, test.Sequential, getter.Get("mode")) + }, + }, + "failure-parallel": { + test: func(t test.Test) { + parent := test.New(t).Mode(test.Parallel).Expect(test.Failure) + child := test.New(parent) + getter := reflect.NewGetter(child) + + assert.Equal(t, test.Failure, getter.Get("expect")) + assert.Equal(t, test.Parallel, getter.Get("mode")) + }, + }, } // TestContext is testing the test context with single simple test cases. func TestContext(t *testing.T) { + t.Parallel() + for name, param := range contextTestCases { - t.Run(name, test.Run(test.Success, func(t test.Test) { + t.Run(name, test.Run(func(t test.Tester) { // Given mock.NewMocks(t).Expect(param.setup) // When - test.New(t, !test.Parallel). + test.New(t).Mode(test.Sequential). Expect(test.Success).Run("", param.test) })) } } -// CleanupParams is a test parameter type for testing the Cleanup method. -type CleanupParams struct { +// cleanupParams is a test parameter type for testing the Cleanup method. +type cleanupParams struct { test test.Func wait int } // cleanupTestCases is a map of test parameters for testing the Cleanup method. -var cleanupTestCases = map[string]CleanupParams{ +var cleanupTestCases = map[string]cleanupParams{ "nil-cleanup": { test: func(t test.Test) { t.Cleanup(nil) @@ -122,8 +513,10 @@ var cleanupTestCases = map[string]CleanupParams{ // TestCleanup is testing the Cleanup method with various scenarios including nil input. func TestCleanup(t *testing.T) { + t.Parallel() + for name, param := range cleanupTestCases { - t.Run(name, test.Run(test.Success, func(t test.Test) { + t.Run(name, test.Run(func(t test.Tester) { // Given wg := sync.NewWaitGroup() wg.Add(param.wait + 1) @@ -131,7 +524,7 @@ func TestCleanup(t *testing.T) { t.Cleanup(func() { wg.Wait() }) // When - test.New(t, test.Parallel). + test.New(t).Mode(test.Parallel). Expect(test.Success).Run("", param.test) // Then @@ -140,18 +533,18 @@ func TestCleanup(t *testing.T) { } } -// ParallelParams is a test parameter type for testing the test context in +// parallelParams is a test parameter type for testing the test context in // conflicting parallel cases resulting in panics. -type ParallelParams struct { - setup mock.SetupFunc - parallel bool - before test.SetupFunc - during test.Func +type parallelParams struct { + setup mock.SetupFunc + mode test.Mode + before test.SetupFunc + during test.Func } // parallelTestCases is a map of test parameters for testing the test context // in conflicting parallel cases resulting in a panics. -var parallelTestCases = map[string]ParallelParams{ +var parallelTestCases = map[string]parallelParams{ "setenv-in-run-without-parallel": { during: func(t test.Test) { t.Setenv("TESTING", "during") @@ -162,7 +555,7 @@ var parallelTestCases = map[string]ParallelParams{ "setenv-in-run-with-parallel": { setup: test.Panic("testing: test using t.Setenv, t.Chdir, or " + "cryptotest.SetGlobalRandom can not use t.Parallel"), - parallel: true, + mode: test.Parallel, during: func(t test.Test) { t.Setenv("TESTING", "during") assert.Equal(t, "during", os.Getenv("TESTING")) @@ -183,7 +576,7 @@ var parallelTestCases = map[string]ParallelParams{ "setenv-before-run-with-parallel": { setup: test.Panic("testing: test using t.Setenv, t.Chdir, or " + "cryptotest.SetGlobalRandom can not use t.Parallel"), - parallel: true, + mode: test.Parallel, before: func(t test.Test) { t.Setenv("TESTING", "before") assert.Equal(t, "before", os.Getenv("TESTING")) @@ -202,7 +595,7 @@ var parallelTestCases = map[string]ParallelParams{ // cases creating panics. func TestContextParallel(t *testing.T) { for name, param := range parallelTestCases { - t.Run(name, test.RunSeq(test.Success, func(t test.Test) { + t.Run(name, test.RunSeq(func(t test.Tester) { // Given if param.before != nil { mock.NewMocks(t).Expect(param.setup) @@ -210,7 +603,7 @@ func TestContextParallel(t *testing.T) { } // When - test.New(t, param.parallel). + test.New(t).Mode(param.mode). Expect(test.Success).Run("", func(t test.Test) { mock.NewMocks(t).Expect(param.setup) param.during(t) @@ -219,13 +612,16 @@ func TestContextParallel(t *testing.T) { } } -type DeadlineParams struct { +// deadlineParams is a test parameter type for testing the Deadline method. +type deadlineParams struct { time, early, sleep time.Duration expect mock.SetupFunc failure test.Expect } -var deadlineTestCases = map[string]DeadlineParams{ +// deadlineTestCases is a map of test parameters for testing the Deadline +// method. +var deadlineTestCases = map[string]deadlineParams{ "failed": { time: 0, early: 0, @@ -267,15 +663,15 @@ var deadlineTestCases = map[string]DeadlineParams{ }, } +// TestDeadline is testing the Deadline method with various scenarios including +// timeouts and early stops. func TestDeadline(t *testing.T) { - t.Parallel() - test.Map(t, deadlineTestCases). Timeout(0).StopEarly(0). - Run(func(t test.Test, param DeadlineParams) { + Run(func(t test.Test, param deadlineParams) { mock.NewMocks(t).Expect(param.expect) - test.New(t, !test.Parallel).Expect(!param.failure). + test.New(t).Mode(test.Sequential).Expect(!param.failure). Timeout(param.time).StopEarly(param.early). Run("", func(t test.Test) { // When diff --git a/test/factory.go b/test/factory.go index cb781fa..fa23cab 100644 --- a/test/factory.go +++ b/test/factory.go @@ -39,7 +39,8 @@ type ParamFunc[P any] func(t Test, param P) // `b.ResetTimer` after the setup phase and before entering the loop. type BenchmarkFunc[P any] func(b *testing.B, param P) func(*testing.B) -// FilterFunc defines the common test filter function signature. +// FilterFunc defines the common test filter function signature expecting the +// test name and parameter set. type FilterFunc[P any] func(name string, param P) bool // CleanupFunc defines the common test cleanup function signature. @@ -290,14 +291,14 @@ func (f *factory[P]) StopEarly(early time.Duration) Factory[P] { // Run runs the test parameter sets (by default) parallel. func (f *factory[P]) Run(call ParamFunc[P]) Factory[P] { return f.dispatch(Parallel, func(name string, param P) { - f.test(name, param, call, Parallel) + f.test(Parallel, name, param, call) }) } // RunSeq runs the test parameter sets in a sequence. func (f *factory[P]) RunSeq(call ParamFunc[P]) Factory[P] { - return f.dispatch(!Parallel, func(name string, param P) { - f.test(name, param, call, !Parallel) + return f.dispatch(Sequential, func(name string, param P) { + f.test(Sequential, name, param, call) }) } @@ -308,7 +309,7 @@ func (f *factory[P]) RunSeq(call ParamFunc[P]) Factory[P] { // Use `test.Benchmark(b)` to wrap the *testing.B target as a Benchmarker when // using the test runner in benchmarks. func (f *factory[P]) Benchmark(call BenchmarkFunc[P]) Factory[P] { - return f.dispatch(!Parallel, func(name string, param P) { + return f.dispatch(Sequential, func(name string, param P) { f.bench(name, param, call) }) } @@ -323,10 +324,10 @@ func (f *factory[P]) Cleanup(call CleanupFunc) { }) } -// Parallel ensures that the test runner runs the test parameter sets in -// parallel. -func (f *factory[P]) parallel(parallel bool) { - if parallel { +// Mode ensures that the test runner runs the test parameter sets in +// the specified mode. +func (f *factory[P]) mode(mode Mode) { + if mode&Parallel == Parallel { defer f.recover() f.t.Parallel() } @@ -346,11 +347,11 @@ func (*factory[P]) recover() { // map and slice cases it also triggers the outer parallel declaration if // enabled. func (f *factory[P]) dispatch( - parallel bool, call func(name string, param P), + mode Mode, call func(name string, param P), ) Factory[P] { switch params := f.params.(type) { case map[string]P: - f.parallel(parallel) + f.mode(mode) for _, key := range f.sorted(params) { param := params[key] name := reflect.Name(key, param) @@ -358,7 +359,7 @@ func (f *factory[P]) dispatch( } case []P: - f.parallel(parallel) + f.mode(mode) for index, param := range params { name := reflect.Name("", param) + "[" + strconv.Itoa(index) + "]" @@ -411,18 +412,18 @@ func (f *factory[P]) filter( // wrap creates the wrapper method eventually executing the test. func (f *factory[P]) wrap( - param P, call ParamFunc[P], parallel bool, + mode Mode, name string, param P, call ParamFunc[P], ) func(Test) { f.wg.Add(1) return func(t Test) { t.Helper() - New(t, parallel). + New(t).Mode(mode). Expect(reflect.Find(param, Success, "expect", "*")). Timeout(reflect.Find(param, f.timeout, "timeout")). StopEarly(reflect.Find(param, f.early, "early")). - Run("", func(t Test) { + Run(name, func(t Test) { t.Helper() defer f.wg.Done() @@ -436,15 +437,16 @@ func (f *factory[P]) wrap( // goroutine and synchronized with the wait group. The test is wrapped to // apply the timeout and early stop configuration. func (f *factory[P]) test( - name string, param P, call ParamFunc[P], parallel bool, + mode Mode, name string, param P, call ParamFunc[P], ) { // Execute anonymous non-parallel tests directly. - if name == "" && !parallel { - f.wrap(param, call, parallel)(f.t) + if name == "" && mode&Parallel == Sequential { + f.wrap(mode, "", param, call)(f.t) return } - New(f.t, !Parallel).Run(name, f.wrap(param, call, parallel)) + New(f.t).Mode(Sequential).Run(name, + f.wrap(mode, "", param, call)) } // bench runs the given single bench parameter set with the given name. diff --git a/test/factory_test.go b/test/factory_test.go index 960fa41..f037ae1 100644 --- a/test/factory_test.go +++ b/test/factory_test.go @@ -19,7 +19,7 @@ func TestParamsRun(t *testing.T) { count := atomic.Int32{} test.Param(t, commonTestCases.GetSlice()...). - Run(func(t test.Test, param TestParams) { + Run(func(t test.Test, param testParams) { defer count.Add(1) param.CheckName(t) param.ExecTest(t) @@ -37,8 +37,8 @@ func TestParamsRunFiltered(t *testing.T) { assert.NotEmpty(t, expect) test.Param(t, commonTestCases.GetSlice()...). - Filter(test.Pattern[TestParams](pattern)). - Run(func(t test.Test, param TestParams) { + Filter(test.Pattern[testParams](pattern)). + Run(func(t test.Test, param testParams) { defer count.Add(1) name := param.name assert.Contains(t, name, pattern) @@ -56,7 +56,7 @@ func TestMapRun(t *testing.T) { count := atomic.Int32{} test.Map(t, commonTestCases). - Run(func(t test.Test, param TestParams) { + Run(func(t test.Test, param testParams) { defer count.Add(1) param.CheckName(t) param.ExecTest(t) @@ -74,8 +74,8 @@ func TestMapRunFiltered(t *testing.T) { assert.NotEmpty(t, expect) test.Map(t, commonTestCases). - Filter(test.Pattern[TestParams](pattern)). - Run(func(t test.Test, param TestParams) { + Filter(test.Pattern[testParams](pattern)). + Run(func(t test.Test, param testParams) { defer count.Add(1) assert.Contains(t, t.Name(), pattern) name := strings.ReplaceAll(t.Name()[19:], "-", " ") @@ -94,7 +94,7 @@ func TestSliceRun(t *testing.T) { count := atomic.Int32{} test.Slice(t, commonTestCases.GetSlice()). - Run(func(t test.Test, param TestParams) { + Run(func(t test.Test, param testParams) { defer count.Add(1) param.CheckName(t) param.ExecTest(t) @@ -112,8 +112,8 @@ func TestSliceRunFiltered(t *testing.T) { assert.NotEmpty(t, expect) test.Slice(t, commonTestCases.GetSlice()). - Filter(test.Pattern[TestParams](pattern)). - Run(func(t test.Test, param TestParams) { + Filter(test.Pattern[testParams](pattern)). + Run(func(t test.Test, param testParams) { defer count.Add(1) name := param.name assert.Contains(t, name, pattern) @@ -134,8 +134,8 @@ func TestRunnerPanic(t *testing.T) { "cryptotest.SetGlobalRandom can not use t.Parallel") t.Setenv("TESTING", "before") - test.Any[ParamParams](t, []ParamParams{{expect: true}}). - Run(func(t test.Test, param ParamParams) { + test.Any[paramParams](t, []paramParams{{expect: true}}). + Run(func(t test.Test, param paramParams) { param.CheckName(t) }) } @@ -144,24 +144,37 @@ func TestRunnerPanic(t *testing.T) { // tests. Currently, I have no idea hot to integrate the test using the above // simplified test pattern that only works on `test.Test` and not `testing.T“. func TestInvalidTypePanic(t *testing.T) { - defer test.Recover(t, test.NewErrInvalidType(ParamParams{})) + defer test.Recover(t, test.NewErrInvalidType(paramParams{})) - test.Any[TestParams](t, ParamParams{expect: false}). - Run(func(t test.Test, param TestParams) { + test.Any[testParams](t, paramParams{expect: false}). + Run(func(t test.Test, param testParams) { param.CheckName(t) }) } +// These types are used to simplify the test cases for the filter and prefix +// tests. type ( - Any = struct{} - FactoryAny = test.Factory[Any] - filterParams struct { - cases map[string]Any - apply func(FactoryAny) FactoryAny - expect map[string]bool - } + // Any is a placeholder type used in filter and prefix tests. + Any = struct{} + // FactoryAny is a type alias for the test factory with Any type, used in + // filter and prefix tests. + FactoryAny = test.Factory[Any] ) +// filterParams defines the parameters for testing the filter functionality of +// the test factory. +type filterParams struct { + cases map[string]Any + apply func(FactoryAny) FactoryAny + expect map[string]bool +} + +// These test cases are testing the filter functionality of the test factory. +// The filter functionality allows selecting specific test cases based on +// various criteria, such as name patterns, OS/arch, or custom logic. The test +// cases cover a wide range of scenarios, including basic filters, combinations +// of filters, and edge cases. var filterCases = map[string]filterParams{ // all filter - always includes all cases "all-single-case": { @@ -752,8 +765,12 @@ var filterCases = map[string]filterParams{ }, } +// TestFilter is testing the filter functionality of the test factory. The +// filter functionality allows selecting specific test cases based on various +// criteria, such as name patterns, OS/arch, or custom logic. The test cases +// cover a wide range of scenarios, including basic filters, combinations of +// filters, and edge cases. func TestFilter(t *testing.T) { - t.Parallel() test.Map(t, filterCases). Run(func(t test.Test, param filterParams) { // Given @@ -773,12 +790,15 @@ func TestFilter(t *testing.T) { }) } +// prefixParams defines the parameters for testing the prefix functionality of +// the test factory. type prefixParams struct { cases map[string]Any apply func(FactoryAny) FactoryAny expect map[string]bool } +// These test cases are testing the prefix functionality of the test factory. var prefixCases = map[string]prefixParams{ "no-prefix": { cases: map[string]Any{ @@ -831,6 +851,7 @@ var prefixCases = map[string]prefixParams{ }, } +// TestPrefix is testing the prefix functionality of the test factory. func TestPrefix(t *testing.T) { test.Map(t, prefixCases). Run(func(t test.Test, param prefixParams) { diff --git a/test/pattern.go b/test/pattern.go index d66b756..dab8c2c 100644 --- a/test/pattern.go +++ b/test/pattern.go @@ -31,6 +31,16 @@ func Must[T any](arg T, err error) T { return arg } +// Okay is a convenience method to check whether the second boolean argument +// is `true` while returning the first argument. If the boolean argument is +// `false`, the method panics. +func Okay[T any](arg T, ok bool) T { + if !ok { + panic("not okay") + } + return arg +} + // Cast is a convenience function to cast the given argument to the specified // type or panic controlled if the cast fails. The method allows to write // concise test setup code granting meaningful type checks. diff --git a/test/pattern_test.go b/test/pattern_test.go index 7eb254a..801a55c 100644 --- a/test/pattern_test.go +++ b/test/pattern_test.go @@ -30,11 +30,11 @@ type ( var testFunc TestFunc = func(a *any) any { return a } -type PtrParams struct { +type ptrParams struct { value any } -var ptrTestCases = map[string]PtrParams{ +var ptrTestCases = map[string]ptrParams{ // Primitive types "bool-true": {value: true}, "bool-false": {value: false}, @@ -106,7 +106,7 @@ var ptrTestCases = map[string]PtrParams{ } func TestPtr(t *testing.T) { - test.Map(t, ptrTestCases).Run(func(t test.Test, param PtrParams) { + test.Map(t, ptrTestCases).Run(func(t test.Test, param ptrParams) { // When result := test.Ptr(param.value) @@ -121,14 +121,14 @@ func TestPtr(t *testing.T) { }) } -type MustParams struct { +type mustParams struct { setup mock.SetupFunc arg any err error expect any } -var mustTestCases = map[string]MustParams{ +var mustTestCases = map[string]mustParams{ "nil": {}, "string": { arg: "value", @@ -207,7 +207,7 @@ var mustTestCases = map[string]MustParams{ } func TestMust(t *testing.T) { - test.Map(t, mustTestCases).Run(func(t test.Test, param MustParams) { + test.Map(t, mustTestCases).Run(func(t test.Test, param mustParams) { // Given mock.NewMocks(t).Expect(param.setup) @@ -224,14 +224,130 @@ func TestMust(t *testing.T) { }) } -type CastParams struct { +type okayParams struct { + setup mock.SetupFunc + arg any + ok bool + expect any +} + +var okayTestCases = map[string]okayParams{ + "nil": { + ok: true, + }, + "string": { + arg: "value", + ok: true, + expect: "value", + }, + "integer": { + arg: 1, + ok: true, + expect: 1, + }, + "float": { + arg: 3.14, + ok: true, + expect: 3.14, + }, + "bool-true": { + arg: true, + ok: true, + expect: true, + }, + "bool-false": { + arg: false, + ok: true, + expect: false, + }, + "slice": { + arg: []string{"a", "b", "c"}, + ok: true, + expect: []string{"a", "b", "c"}, + }, + "map": { + arg: map[string]int{"key": 42}, + ok: true, + expect: map[string]int{"key": 42}, + }, + "struct": { + arg: TestStruct{name: "test", id: 123}, + ok: true, + expect: TestStruct{name: "test", id: 123}, + }, + "pointer": { + arg: &TestStruct{name: "pointer", id: 456}, + ok: true, + expect: &TestStruct{name: "pointer", id: 456}, + }, + "function": { + arg: testFunc, + ok: true, + expect: testFunc, + }, + "named-slice": { + arg: TestSlice{"x", "y", "z"}, + ok: true, + expect: TestSlice{"x", "y", "z"}, + }, + "named-map": { + arg: TestMap{"foo": 1, "bar": 2}, + ok: true, + expect: TestMap{"foo": 1, "bar": 2}, + }, + "zero-value-int": { + arg: 0, + ok: true, + expect: 0, + }, + "zero-value-string": { + arg: "", + ok: true, + expect: "", + }, + "empty-slice": { + arg: []string{}, + ok: true, + expect: []string{}, + }, + "empty-map": { + arg: map[string]int{}, + ok: true, + expect: map[string]int{}, + }, + "not-okay": { + setup: test.Panic("not okay"), + ok: false, + expect: nil, + }, +} + +func TestOkay(t *testing.T) { + test.Map(t, okayTestCases).Run(func(t test.Test, param okayParams) { + // Given + mock.NewMocks(t).Expect(param.setup) + + // When + result := test.Okay(param.arg, param.ok) + + // Then + if strings.Contains(t.Name(), "function") { + // For functions, just verify the result is not nil (functions can't be compared) + assert.NotNil(t, result) + } else { + assert.Equal(t, param.expect, result) + } + }) +} + +type castParams struct { setup mock.SetupFunc arg any cast func(arg any) any expect any } -var castTestCases = map[string]CastParams{ +var castTestCases = map[string]castParams{ "int-to-int": { arg: 42, cast: func(arg any) any { return test.Cast[int](arg) }, @@ -371,7 +487,7 @@ var castTestCases = map[string]CastParams{ } func TestCast(t *testing.T) { - test.Map(t, castTestCases).Run(func(t test.Test, param CastParams) { + test.Map(t, castTestCases).Run(func(t test.Test, param castParams) { // Given mock.NewMocks(t).Expect(param.setup) @@ -394,13 +510,13 @@ func TestCast(t *testing.T) { }) } -type RecoverParams struct { +type recoverParams struct { setup any expect test.Expect panic any } -var recoverTestCases = map[string]RecoverParams{ +var recoverTestCases = map[string]recoverParams{ // Failure to panic. "no-panic-with-nil": {}, "no-panic-with-string": { @@ -433,7 +549,7 @@ var recoverTestCases = map[string]RecoverParams{ func TestRecover(t *testing.T) { test.Map(t, recoverTestCases). - Run(func(t test.Test, param RecoverParams) { + Run(func(t test.Test, param recoverParams) { // Given defer test.Recover(t, param.setup) @@ -660,13 +776,13 @@ func (d mapPtrDeepCopyObject) DeepCopyObject() mapPtrDeepCopyObject { return copied } -// DeepCopyCasesParams defines parameters for testing DeepCopyTestCases. -type DeepCopyCasesParams struct { +// deepCopyTestCasesParams defines parameters for testing DeepCopyTestCases. +type deepCopyTestCasesParams struct { args []any expect map[string]test.DeepCopyParams } -var deepCopyTestCasesTestCases = map[string]DeepCopyCasesParams{ +var deepCopyTestCasesTestCases = map[string]deepCopyTestCasesParams{ "struct-anno": { args: []any{&struct{ Value int }{}}, expect: map[string]test.DeepCopyParams{ @@ -835,7 +951,7 @@ var deepCopyTestCasesTestCases = map[string]DeepCopyCasesParams{ func TestDeepCopyTestCases(t *testing.T) { test.Map(t, deepCopyTestCasesTestCases). - Run(func(t test.Test, param DeepCopyCasesParams) { + Run(func(t test.Test, param deepCopyTestCasesParams) { // When cases := test.DeepCopyTestCases(42, 3, 10, param.args...) @@ -844,12 +960,12 @@ func TestDeepCopyTestCases(t *testing.T) { }) } -type DeepCopyParams struct { +type deepCopyParams struct { test.DeepCopyParams setup mock.SetupFunc } -var deepCopyTestCases = map[string]DeepCopyParams{ +var deepCopyTestCases = map[string]deepCopyParams{ // Invalid value. "invalid-nil": { DeepCopyParams: test.DeepCopyParams{ @@ -1080,7 +1196,7 @@ var deepCopyTestCases = map[string]DeepCopyParams{ func TestDeepCopy(t *testing.T) { test.Map(t, deepCopyTestCases). - Run(func(t test.Test, param DeepCopyParams) { + Run(func(t test.Test, param deepCopyParams) { // Given mock.NewMocks(t).Expect(param.setup) diff --git a/test/reporter.go b/test/reporter.go index 959f0e5..a2eeeaf 100644 --- a/test/reporter.go +++ b/test/reporter.go @@ -61,7 +61,7 @@ func NewValidator(ctrl *gomock.Controller) *Validator { if t, ok := ctrl.T.(*Context); ok { // We need to install a second isolated test environment to break the // reporter cycle on the failure issued by the mock controller. - ctrl.T = New(t.t, t.parallel).Expect(t.expect) + ctrl.T = New(t.t).Mode(t.mode).Expect(t.expect) t.expect = false t.Reporter(validator) } @@ -302,7 +302,7 @@ func MissingCalls( // Creates a new mock controller and test environment to isolate the // validator used for sub-call creation/registration from the validator // used for execution. - mocks := mock.NewMocks(New(t, false).Expect(Failure)) + mocks := mock.NewMocks(New(t).Mode(Sequential).Expect(Failure)) calls := make([]func(*mock.Mocks) any, 0, len(setups)) for _, setup := range setups { calls = append(calls, diff --git a/test/reporter_test.go b/test/reporter_test.go index 296775f..c9eecfc 100644 --- a/test/reporter_test.go +++ b/test/reporter_test.go @@ -18,7 +18,7 @@ var ( errOther = errors.New(otherError) ) -type MatcherParams struct { +type matcherParams struct { matcher func(any) gomock.Matcher base any match any @@ -26,7 +26,7 @@ type MatcherParams struct { expectString string } -var errorMatcherTestCases = map[string]MatcherParams{ +var errorMatcherTestCases = map[string]matcherParams{ "success-string-string": { matcher: test.EqError, base: anErrorString, @@ -102,7 +102,7 @@ var errorMatcherTestCases = map[string]MatcherParams{ func TestErrorMatcher(t *testing.T) { test.Map(t, errorMatcherTestCases). - Run(func(t test.Test, param MatcherParams) { + Run(func(t test.Test, param matcherParams) { // Given matcher := param.matcher(param.base) @@ -115,7 +115,7 @@ func TestErrorMatcher(t *testing.T) { }) } -var callMatcherTestCases = map[string]MatcherParams{ +var callMatcherTestCases = map[string]matcherParams{ "success-call-call": { matcher: test.EqCall, base: test.Errorf("%s", "fail"), @@ -143,9 +143,9 @@ func evalCall(arg any, mocks *mock.Mocks) any { func TestCallMatcher(t *testing.T) { test.Map(t, callMatcherTestCases). - Run(func(t test.Test, param MatcherParams) { + Run(func(t test.Test, param matcherParams) { // Given - send mock calls to unchecked test context. - mocks := mock.NewMocks(test.New(t, false).Expect(test.Success)) + mocks := mock.NewMocks(test.New(t).Expect(test.Success)) matcher := param.matcher(evalCall(param.base, mocks)) // When @@ -157,14 +157,14 @@ func TestCallMatcher(t *testing.T) { }) } -type ReporterParams struct { +type reporterParams struct { setup mock.SetupFunc misses func(test.Test, *mock.Mocks) mock.SetupFunc call test.Func expect test.Expect } -var reporterTestCases = map[string]ReporterParams{ +var reporterTestCases = map[string]reporterParams{ "log-called": { setup: test.Log("log message"), call: func(t test.Test) { @@ -486,12 +486,12 @@ var reporterTestCases = map[string]ReporterParams{ func TestReporter(t *testing.T) { test.Map(t, reporterTestCases). - Run(func(t test.Test, param ReporterParams) { + Run(func(t test.Test, param reporterParams) { // Given mocks := mock.NewMocks(t) // When - test.InRun(test.Success, func(tt test.Test) { + test.New(t).Run("", func(tt test.Test) { // Given imocks := mock.NewMocks(tt) if param.misses != nil { @@ -505,6 +505,6 @@ func TestReporter(t *testing.T) { // When param.call(tt) - })(t) + }) }) }