Skip to content

Commit

Permalink
fix: parallel test support (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkrop committed Nov 11, 2022
1 parent 0f7d38e commit da481d7
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 106 deletions.
50 changes: 48 additions & 2 deletions mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,51 @@ unlocks the waiting test in case of failures:
func TestUnitCall(t *testing.T) {
for message, param := range testUnitCallParams {
t.Run(message, test.Success(func(t *testing.T) {
require.NotEmpty(t, message)
//Given
unit, mocks := SetupTestUnit(t, param.mockSetup)

//When
result, err := unit.UnitCall(...)

mocks.Wait()

//Then
if param.expectError != nil {
assert.Equal(t, param.expectError, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, param.expect*, result)
}, false))
}
}
```


## Paralle parameterized test pattern

Generally, the [test](../test) and [mock](.) framework supports parallel test
execution, however, there are some general requirements for running tests in
parallel:

1. The tests *must not modify* environment variables dynamically.
2. The tests *must not require* reserved service ports and open listeners.
3. The tests *must not use* [monkey patching](https://github.com/bouk/monkey)
to modify commonly used functions,
4. The tests *must not use* [gock](https://github.com/h2non/gock) for mocking
on HTTP transport level, and finally
5. The tests *must not share* any other resources, e.g. objects or database
schemas, that need to be updated during the test execution.

If this conditions hold, the general pattern provided above can be extened to
support parallel test execution.

```go
func TestUnitCall(t *testing.T) {
t.Parallel()
for message, param := range testUnitCallParams {
message, param := message, param
t.Run(message, test.Success(func(t *testing.T) {
//Given
unit, mocks := SetupTestUnit(t, param.mockSetup)

Expand All @@ -203,7 +246,10 @@ func TestUnitCall(t *testing.T) {
require.NoError(t, err)
}
assert.Equal(t, param.expect*, result)
}))
}, true))
}
}
```

**Note:** In the above pattern the setup for parallel tests hidden in the setup
of the isolated [test](../test) environment.
43 changes: 37 additions & 6 deletions mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,37 @@ func (mocks *Mocks) syncWith(t gomock.TestReporter) *Mocks {
}

// Wait waits for all mock calls registered via `mocks.Times(<#>)` to be
// consumed before testing continuing. This can be used to support testing of
// detached `go-routines` using isolated test environments.
// consumed before testing continuing. This method implements the `WaitGroup`
// interface to support testing of detached `go-routines` in an isolated
// [test](../test) environment.
func (mocks *Mocks) Wait() {
mocks.wg.Wait()
}

// Times is adding creating the expectation that exactly the given number of
// mock calls setups are consumed via `gomock.Do`.
// TODO: not needed yet - optional extension.
//
// Add adds the given delta on the waiting group handling the expected or
// consumed mock calls. This method implements the `WaitGroup` interface to
// support testing of detached `go-routines` in an isolated [test](../test)
// environment.
//
// func (mocks *Mocks) Add(delta int) {
// mocks.wg.Add(delta)
// }

// TODO: not needed yet - optional extension.
//
// Done removes exactly one expected mock call from the wait group handling the
// expected or consumed mock calls. This method implements the `WaitGroup`
// interface to support testing of detached `go-routines` in an isolated
// [test](../test) environment.
//
// func (mocks *Mocks) Done() {
// mocks.wg.Done()
// }

// Times is creating the expectation that exactly the given number of mock call
// are consumed via `gomock.Do`.
func (mocks *Mocks) Times(num int) int {
mocks.wg.Add(num)
return num
Expand Down Expand Up @@ -158,13 +181,21 @@ func (mocks *Mocks) GetDone(numargs int) any {
}
}

// TODO: Reconsider this apporach. Seems not to be helpful yet.
// TODO: Reconsider this apporach. Seems not to be helpful yet. Test setup
// functions would look as follows:
//
// func GetTokenX(url string, err error) mock.SetupFunc {
// return mock.Mock(NewMockTokenProvider, func(mock *MockTokenProvider) *gomock.Call {
// return mock.EXPECT().GetToken(url).Return(token)
// })
// }
//
// Mock defines an advanced mock setup function for exactly one mock call setup
// by resolving the singleton mock instance and handing it over to the provided
// function for calling the mock method and providing the return values. The
// created function automatically sets up the wait group for advanced testing
// strategies.
//
// func Mock[T any](
// creator func(*Controller) *T, caller func(*T) *gomock.Call,
// ) SetupFunc {
Expand All @@ -174,7 +205,7 @@ func (mocks *Mocks) GetDone(numargs int) any {
// value := reflect.ValueOf(call).Elem()
// field := value.FieldByName("methodType")
// ftype := *(*reflect.Type)(unsafe.Pointer(field.UnsafeAddr()))
// return call.Do(mocks.Done(ftype.NumIn()))
// return call.Do(mocks.GetDone(ftype.NumIn()))
// }
// }

Expand Down
50 changes: 30 additions & 20 deletions mock/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func MockSetup(t gomock.TestReporter, mockSetup mock.SetupFunc) *mock.Mocks {
}

func MockValidate(
t *test.TestingT, mocks *mock.Mocks, validate func(*test.TestingT, *mock.Mocks), failing bool,
t *test.TestingT, mocks *mock.Mocks,
validate func(*test.TestingT, *mock.Mocks),
failing bool,
) {
if failing {
// we need to execute failing test synchronous, since we setup full
Expand Down Expand Up @@ -123,7 +125,9 @@ var testSetupParams = perm.ExpectMap{
}

func TestSetup(t *testing.T) {
t.Parallel()
for message, expect := range testSetupParams.Remain(test.ExpectSuccess) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

Expand All @@ -144,7 +148,7 @@ func TestSetup(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand All @@ -153,7 +157,9 @@ var testChainParams = perm.ExpectMap{
}

func TestChain(t *testing.T) {
t.Parallel()
for message, expect := range testChainParams.Remain(test.ExpectFailure) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

Expand All @@ -174,7 +180,7 @@ func TestChain(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand All @@ -188,7 +194,9 @@ var testSetupChainParams = perm.ExpectMap{
}

func TestSetupChain(t *testing.T) {
t.Parallel()
for message, expect := range testSetupChainParams.Remain(test.ExpectFailure) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

Expand All @@ -213,12 +221,14 @@ func TestSetupChain(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

func TestChainSetup(t *testing.T) {
t.Parallel()
for message, expect := range testSetupChainParams.Remain(test.ExpectFailure) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

Expand All @@ -243,7 +253,7 @@ func TestChainSetup(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand All @@ -263,7 +273,9 @@ var testParallelChainParams = perm.ExpectMap{
}

func TestParallelChain(t *testing.T) {
t.Parallel()
for message, expect := range testParallelChainParams.Remain(test.ExpectFailure) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

Expand All @@ -290,7 +302,7 @@ func TestParallelChain(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand All @@ -314,12 +326,12 @@ var testChainSubParams = perm.ExpectMap{
}

func TestChainSub(t *testing.T) {
t.Parallel()
perms := testChainSubParams
// perms := PermRemain(testChainSubParams, test.ExpectFailure)
for message, expect := range perms {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

// Given
perm := strings.Split(message, "-")
mockSetup := mock.Chain(
Expand All @@ -341,7 +353,7 @@ func TestChainSub(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand All @@ -357,10 +369,10 @@ var testDetachParams = perm.ExpectMap{
}

func TestDetach(t *testing.T) {
t.Parallel()
for message, expect := range testDetachParams.Remain(test.ExpectFailure) {
message, expect := message, expect
t.Run(message, test.Run(expect, func(t *test.TestingT) {
require.NotEmpty(t, message)

// Given
perm := strings.Split(message, "-")
mockSetup := mock.Chain(
Expand All @@ -376,7 +388,7 @@ func TestDetach(t *testing.T) {

// Then
test.Test(t, perm, expect)
}))
}, false))
}
}

Expand Down Expand Up @@ -419,10 +431,10 @@ var testPanicParams = map[string]struct {
}

func TestPanic(t *testing.T) {
t.Parallel()
for message, param := range testPanicParams {
message, param := message, param
t.Run(message, func(t *testing.T) {
require.NotEmpty(t, message)

// Given
defer func() {
err := recover()
Expand Down Expand Up @@ -487,12 +499,10 @@ var testGetSubSliceParams = map[string]struct {
}

func TestGetSubSlice(t *testing.T) {
t.Parallel()
for message, param := range testGetSubSliceParams {
message, param := message, param
t.Run(message, func(t *testing.T) {
require.NotEmpty(t, message)

// Given

// When
slice := mock.GetSubSlice(param.from, param.to, param.slice)

Expand Down Expand Up @@ -521,10 +531,10 @@ var testGetDoneParams = map[string]struct {
}

func TestGetDone(t *testing.T) {
t.Parallel()
for message, param := range testGetDoneParams {
message, param := message, param
t.Run(message, func(t *testing.T) {
require.NotEmpty(t, message)

// Given
mocks := MockSetup(t, nil)
mocks.Times(1)
Expand Down
39 changes: 16 additions & 23 deletions perm/perm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/tkrop/testing/mock"
"github.com/tkrop/testing/test"
"github.com/tkrop/testing/utils/slices"
)

// ExpectMap defines a map of permutation tests that are expected to either
Expand Down Expand Up @@ -58,30 +59,22 @@ func (p *Test) Test(t *test.TestingT, perm []string, expect test.Expect) {
// Remain calculate and add the missing permutations and add it with
// expected result to the given permmutation map.
func (perms ExpectMap) Remain(expect test.Expect) ExpectMap {
for key := range perms {
Slice(strings.Split(key, "-"), func(perm []string) {
key := strings.Join(perm, "-")
if _, ok := perms[key]; !ok {
perms[key] = expect
}
}, 0)
break // we only need to permutate the first key.
cperms := ExpectMap{}
for key, value := range perms {
cperms[key] = value
}
return perms
}

// Slice permutates the given slice starting at the position given by and
// call the `do` function on each permutation to collect the result. For a full
// permutation the `index` must start with `0`.
func Slice[T any](slice []T, do func([]T), index int) {
if index <= len(slice) {
Slice(slice, do, index+1)
for offset := index + 1; offset < len(slice); offset++ {
slice[index], slice[offset] = slice[offset], slice[index]
Slice(slice, do, index+1)
slice[index], slice[offset] = slice[offset], slice[index]
}
} else {
do(slice)
// we only need to permutate the first key.
for key := range cperms {
slices.Permute(strings.Split(key, "-"),
func(perm []string) {
key := strings.Join(perm, "-")
if _, ok := cperms[key]; !ok {
cperms[key] = expect
}
}, 0)
break
}

return cperms
}
Loading

0 comments on commit da481d7

Please sign in to comment.