Skip to content

Commit

Permalink
feat: add setenv and error support (#85)
Browse files Browse the repository at this point in the history
Signed-off-by: Tronje Krop <[email protected]>
  • Loading branch information
Tronje Krop committed Sep 21, 2024
1 parent 272fdce commit 73b61b2
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Makefile.vars
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ CODACY_API_BASE_URL := https://api.codacy.com
# Custom linters applied to prepare next level (default: <empty>).
LINTERS_CUSTOM := nonamedreturns gochecknoinits tagliatelle
# Linters swithed off to complete next level (default: <empty>).
LINTERS_DISABLED := depguard
LINTERS_DISABLED :=
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.14
0.0.15
8 changes: 8 additions & 0 deletions internal/mock/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ var (
Params: []*Param{},
Results: []*Param{},
Variadic: false,
}, {
Name: "Setenv",
Params: []*Param{
{Name: "key", Type: "string"},
{Name: "value", Type: "string"},
},
Results: []*Param{},
Variadic: false,
}, {
Name: "TempDir",
Params: []*Param{},
Expand Down
16 changes: 8 additions & 8 deletions mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ common mock controller interface for [`gomock`][gomock] and [`gock`][gock] that
enables a unified, highly reusable integration pattern.

Unfortunately, we had to sacrifice a bit of type-safety to allow for chaining
mock calls in an arbitrary way during setup. Anyhow, the offered in runtime
validation is a sufficient strategy to cover for the missing type-safety.
mock calls arbitrarily during setup. Anyhow, the offered in runtime validation
is a sufficient strategy to cover for the missing type-safety.


## Example usage
Expand Down Expand Up @@ -39,8 +39,8 @@ patterns as described in the following.

## Generic mock controller setup

Usually, a new system under test must be create for each test run. Therefore,
the following generic pattern to setup the mock controller with an arbitrary
Usually, a new system under test must be created for each test run. Therefore,
the following generic pattern to set up the mock controller with an arbitrary
system under test is very useful.

```go
Expand Down Expand Up @@ -91,7 +91,7 @@ completion via `Do|DoAndReturn()`. For test with detached *goroutines* the
test can wait via `mocks.Wait()`, before finishing and checking whether the
mock calls are completely consumed.

Since some arguments needed to setup a mock call may only be available after
Since some arguments needed to set up a mock call may only be available after
creating the test runner, the mock controller provides a dynamic key-value
storage that is accessible via `SetArg(key,value)`, `SetArgs(map[key]value)`,
and `GetArg(key)`.
Expand Down Expand Up @@ -142,16 +142,16 @@ With the above preparations for mocking service calls we can now define the
with other setup methods that determine the predecessors and successor mock
calls.

* `Parallel` allows to creates an unordered set of mock calls that can be
* `Parallel` allows to create an unordered set of mock calls that can be
combined with other setup methods that determine the predecessor and
successor mock calls.

* `Setup` allows to create an unordered detached set of mock calls that creates
no relation to predecessors and successors it was defined with.

Beside this simple (un-)ordering methods there are two further methods for
completeness, that allow to control how predecessors and successors are used
to setup ordering conditions:
completeness, that allows control of how predecessors and successors are used
to set up ordering conditions:

* `Sub` allows to define a sub-set or sub-chain of elements in `Parallel` and
`Chain` as predecessor and successor context for further combination.
Expand Down
4 changes: 2 additions & 2 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Or the interface of the underlying `test.Tester`:
func TestUnit(t *testing.T) {
t.Parallel()

test.Tester(t, test.Success).Run(func(t test.Test){
test.NewTester(t, test.Success).Run(func(t test.Test){
// Given

// When
Expand All @@ -144,7 +144,7 @@ validator that is tightly integrated with the [`mock`](../mock) framework.
func TestUnit(t *testing.T) {
test.Run(func(t test.Test){
// Given
mocks := mock.NewMocks(t).Expect(mock.Setup(
mock.NewMocks(t).Expect(mock.Setup(
test.Errorf("fail"),
test.Fatalf("fail"),
test.FailNow(),
Expand Down
2 changes: 1 addition & 1 deletion test/caller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ var (
}()
// CallerTestErrorf provides the file with the line number of the `Errorf`
// call in testing.
CallerTestErrorf = path.Join(SourceDir, "testing.go:204")
CallerTestErrorf = path.Join(SourceDir, "testing.go:206")
// CallerGomockErrorf provides the file with the line number of the
// `Errorf` call in gomock.
CallerGomockErrorf = path.Join(SourceDir, "gomock.go:61")
Expand Down
52 changes: 52 additions & 0 deletions test/reflect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package test

import (
"reflect"
"unsafe"
)

// Error creates an Accessor for the given error to access and modify its
// unexported fields by field name.
//
// Example:
//
// err := test.Error(errors.New("error message")).Set("text", "new message").Get("")
// fmt.Println(err.Error()) // Output: new message
//
// err := test.Error(errors.New("error message")).Set("text", "new message").Get("text")
// fmt.Println(err) // Output: new message
func Error(err error) *Accessor[error] {
return NewAccessor[error](err)
}

// Accessor allows you to access and modify unexported fields of a struct.
type Accessor[T any] struct {
target T
}

// NewAccessor creates a generic accessor for the given target.
func NewAccessor[T any](target T) *Accessor[T] {
return &Accessor[T]{target: target}
}

// Set sets the value of the accessor target's field with the given name.
func (a *Accessor[T]) Set(name string, value any) *Accessor[T] {
field := reflect.ValueOf(a.target).Elem().FieldByName(name)
// #nosec G103,G115 // This is a safe use of unsafe.Pointer.
reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).
Elem().Set(reflect.ValueOf(value))

return a
}

// Get returns the value of the field with the given name. If the name is empty,
// it returns the accessor target itself.
func (a *Accessor[T]) Get(name string) any {
if name == "" {
return a.target
}
field := reflect.ValueOf(a.target).Elem().FieldByName(name)
// #nosec G103,G115 // This is a safe use of unsafe.Pointer.
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).
Elem().Interface()
}
53 changes: 53 additions & 0 deletions test/reflect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package test_test

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/tkrop/go-testing/test"
)

type testErrorParam struct {
error error
setup func(*test.Accessor[error])
test func(test.Test, *test.Accessor[error])
}

var testErrorParams = map[string]testErrorParam{
"test get": {
error: errors.New("test get"),
test: func(t test.Test, a *test.Accessor[error]) {
assert.Equal(t, "test get", a.Get("s"))
assert.Equal(t, errors.New("test get"), a.Get(""))
},
},

"test set": {
error: errors.New("test set"),
setup: func(a *test.Accessor[error]) {
a.Set("s", "test set first").
Set("s", "test set final")
},
test: func(t test.Test, a *test.Accessor[error]) {
assert.Equal(t, "test set final", a.Get("s"))
assert.Equal(t, errors.New("test set final"), a.Get(""))
},
},
}

func TestError(t *testing.T) {
test.Map(t, testErrorParams).
Run(func(t test.Test, param testErrorParam) {
// Given
accessor := test.Error(param.error)

// When
if param.setup != nil {
param.setup(accessor)
}

// The
param.test(t, accessor)
})
}
76 changes: 51 additions & 25 deletions test/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,6 @@ const (
Parallel = true
)

// ensureParallel ensures that the test runs test parameter sets in parallel.
func ensureParallel(t *testing.T) {
t.Helper()
defer func() {
if v := recover(); v != nil &&
v != "testing: t.Parallel called multiple times" {
panic(v)
}
}()
t.Parallel()
}

// TODO: consider following convenience methods:
//
// // Result is a convenience method that returns the first argument ans swollows
Expand Down Expand Up @@ -87,12 +75,18 @@ func ensureParallel(t *testing.T) {
// return result
// }

// Reporter is a minimal inferface for abstracting test report methods that are
// Reporter is a minimal interface for abstracting test report methods that are
// needed to setup an isolated test environment for GoMock and Testify.
type Reporter interface {
// Panic reports a panic.
Panic(arg any)
// Errorf reports a failure messages when a test is supposed to continue.
Errorf(format string, args ...any)
// Fatalf reports a fatal failure message that immediate aborts of the test
// execution.
Fatalf(format string, args ...any)
// FailNow reports fatal failure notifications without log output that
// aborts test execution immediately.
FailNow()
}

Expand All @@ -110,12 +104,14 @@ type Test interface {
TempDir() string
// Errorf handles a failure messages when a test is supposed to continue.
Errorf(format string, args ...any)
// Fatalf handles a fatal failure messge that immediate aborts of the test
// Fatalf handles a fatal failure message that immediate aborts of the test
// execution.
Fatalf(format string, args ...any)
// FailNow handles fatal failure notifications without log output that
// aborts test execution immediately.
FailNow()
// Setenv sets an environment variable for the test.
Setenv(key, value string)
}

// Cleanuper defines an interface to add a custom mehtod that is called after
Expand Down Expand Up @@ -179,12 +175,18 @@ func (t *Tester) Helper() {
t.t.Helper()
}

// Parallel delegates request to the parent context if it is of type
// `*testing.T`. Else it is swallowing the request silently.
// 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 *Tester) Parallel() {
if t, ok := t.t.(*testing.T); ok {
ensureParallel(t)
}
defer t.recoverParallel()
t.t.Parallel()
}

// Setenv delegates request to the parent context, if it is of type
// `*testing.T`. Else it is swallowing the request silently.
func (t *Tester) Setenv(key, value string) {
t.t.Setenv(key, value)
}

// TempDir delegates the request to the parent test context.
Expand All @@ -205,7 +207,7 @@ func (t *Tester) Errorf(format string, args ...any) {
}
}

// Fatalf handles a fatal failure messge that immediate aborts of the test
// Fatalf handles a fatal 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.
Expand Down Expand Up @@ -334,12 +336,25 @@ func (t *Tester) finish() {
func (t *Tester) recover() {
t.Helper()

//revive:disable-next-line:defer // is inside the defered function
//revive:disable-next-line:defer // only used inside a deferred call.
if arg := recover(); arg != nil {
t.Panic(arg)
}
}

// recoverParallel recovers from panics when calling `t.Parallel()` multiple
// times.
func (t *Tester) recoverParallel() {
t.Helper()

//revive:disable-next-line:defer // only used inside a deferred call.
if v := recover(); v != nil &&
v != "testing: t.Parallel called multiple times" {
// TODO: t.Panic(v)
panic(v)
}
}

// unlock unlocks the wait group of the test by consuming the wait group
// counter completely.
func (t *Tester) unlock() {
Expand Down Expand Up @@ -414,14 +429,25 @@ func (r *runner[P]) Run(call func(t Test, param P)) Runner[P] {

// RunSeq runs the test parameter sets in a sequence.
func (r *runner[P]) RunSeq(call func(t Test, param P)) Runner[P] {
return r.run(call, false)
return r.run(call, !Parallel)
}

// parallel ensures that the test runner runs the test parameter sets in
// parallel.
func (r *runner[P]) parallel(parallel bool) {
if parallel {
ensureParallel(r.t)
defer r.recoverParallel()
r.t.Parallel()
}
}

// recoverParallel recovers from panics when calling `t.Parallel()` multiple
// times.
func (*runner[P]) recoverParallel() {
//revive:disable-next-line:defer // only used inside a deferred call.
if v := recover(); v != nil &&
v != "testing: t.Parallel called multiple times" {
panic(v)
}
}

Expand Down Expand Up @@ -506,7 +532,7 @@ func Run(expect Expect, test func(Test)) func(*testing.T) {
// with given expectation. When executed via `t.Run()` it checks whether the
// result is matching the expectation.
func RunSeq(expect Expect, test func(Test)) func(*testing.T) {
return run(expect, test, false)
return run(expect, test, !Parallel)
}

// Run creates an isolated parallel or sequential test environment running the
Expand All @@ -527,7 +553,7 @@ func InRun(expect Expect, test func(Test)) func(Test) {
return func(t Test) {
t.Helper()

NewTester(t, expect).Run(test, false)
NewTester(t, expect).Run(test, !Parallel)
}
}

Expand Down
Loading

0 comments on commit 73b61b2

Please sign in to comment.