Skip to content

Commit

Permalink
assert package API change + random.Unique helper
Browse files Browse the repository at this point in the history
Align the `AnyOf` assertion helper in the assert package with other existing assertion helper functions.

Introduce a `random.Unique` utility function to simplify the generation of unique random values in tests,
reducing the risk of flaky test results.
  • Loading branch information
adamluzsi committed Oct 25, 2023
1 parent cade259 commit dc2e612
Show file tree
Hide file tree
Showing 19 changed files with 448 additions and 109 deletions.
6 changes: 3 additions & 3 deletions Spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1097,9 +1097,9 @@ func TestSpec_Test_flakyByStrategy_willRunAgainBasedOnTheStrategy(t *testing.T)
}
}, testcase.Flaky(strategy))

assert.Must(t).AnyOf(func(a *assert.AnyOf) {
a.Test(func(t assert.It) { t.Must.Equal(strategyCallCount, testCount) })
a.Test(func(t assert.It) { t.Must.Equal(strategyCallCount+1, testCount) }) // when there is no error, the total
assert.Must(t).AnyOf(func(a *assert.A) {
a.Case(func(t assert.It) { t.Must.Equal(strategyCallCount, testCount) })
a.Case(func(t assert.It) { t.Must.Equal(strategyCallCount+1, testCount) }) // when there is no error, the total
})
}

Expand Down
38 changes: 14 additions & 24 deletions assert/AnyOf.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,14 @@ import (
"go.llib.dev/testcase/internal/fmterror"
)

// OneOf function checks a list of values and matches an expectation against each element of the list.
// If any of the elements pass the assertion, then the assertion helper function does not fail the test.
func OneOf[V any](tb testing.TB, vs []V, blk func(it It, got V), msg ...Message) {
tb.Helper()
Must(tb).AnyOf(func(a *AnyOf) {
a.name = "OneOf"
a.cause = "None of the element matched the expectations"
for _, v := range vs {
a.Test(func(it It) { blk(it, v) })
if a.OK() {
break
}
}
}, msg...)
}

// AnyOf is an assertion helper that allows you run AnyOf.Test assertion blocks, that can fail, as lone at least one of them succeeds.
// A stands for Any Of, an assertion helper that allows you run A.Case assertion blocks, that can fail, as lone at least one of them succeeds.
// common usage use-cases:
// - list of interface, where test order, or the underlying structure's implementation is irrelevant for the behavior.
// - list of big structures, where not all field value relevant, only a subset, like a structure it wraps under a field.
// - list of structures with fields that has dynamic state values, which is irrelevant for the given test.
// - structure that can have various state scenario, and you want to check all of them, and you expect to find one match with the input.
// - fan out scenario, where you need to check in parallel that at least one of the worker received the event.
type AnyOf struct {
type A struct {
TB testing.TB
Fail func()

Expand All @@ -44,10 +28,10 @@ type AnyOf struct {
cause string
}

// Test will test a block of assertion that must succeed in order to make AnyOf pass.
// You can have as much AnyOf.Test calls as you need, but if any of them pass with success, the rest will be skipped.
// Using Test is safe for concurrently.
func (ao *AnyOf) Test(blk func(t It)) {
// Case will test a block of assertion that must succeed in order to make A pass.
// You can have as much A.Case calls as you need, but if any of them pass with success, the rest will be skipped.
// Using Case is safe for concurrently.
func (ao *A) Case(blk func(t It)) {
ao.TB.Helper()
if ao.OK() {
return
Expand All @@ -70,8 +54,14 @@ func (ao *AnyOf) Test(blk func(t It)) {
return
}

// Test is an alias for A.Case
func (ao *A) Test(blk func(t It)) {
ao.TB.Helper()
ao.Test(blk)
}

// Finish will check if any of the assertion succeeded.
func (ao *AnyOf) Finish(msg ...Message) {
func (ao *A) Finish(msg ...Message) {
ao.TB.Helper()
if ao.OK() {
return
Expand All @@ -95,7 +85,7 @@ func (ao *AnyOf) Finish(msg ...Message) {
ao.Fail()
}

func (ao *AnyOf) OK() bool {
func (ao *A) OK() bool {
ao.mutex.Lock()
defer ao.mutex.Unlock()
return ao.passed
Expand Down
28 changes: 14 additions & 14 deletions assert/AnyOf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import (
"go.llib.dev/testcase/internal/doubles"
)

func TestAnyOf(t *testing.T) {
func TestA(t *testing.T) {
s := testcase.NewSpec(t)

stub := testcase.Let(s, func(t *testcase.T) *doubles.TB {
return &doubles.TB{}
})
anyOf := testcase.Let(s, func(t *testcase.T) *assert.AnyOf {
return &assert.AnyOf{TB: stub.Get(t), Fail: stub.Get(t).Fail}
anyOf := testcase.Let(s, func(t *testcase.T) *assert.A {
return &assert.A{TB: stub.Get(t), Fail: stub.Get(t).Fail}
})
subject := func(t *testcase.T, blk func(it assert.It)) {
anyOf.Get(t).Test(blk)
anyOf.Get(t).Case(blk)
}

s.When(`there is at least one .Test with non failing ran`, func(s *testcase.Spec) {
s.When(`there is at least one .Case with non failing ran`, func(s *testcase.Spec) {
s.Before(func(t *testcase.T) {
subject(t, func(it assert.It) { /* no fail */ })
})
Expand All @@ -39,7 +39,7 @@ func TestAnyOf(t *testing.T) {
t.Must.True(anyOf.Get(t).OK())
})

s.And(`and new .Test calls are made`, func(s *testcase.Spec) {
s.And(`and new .Case calls are made`, func(s *testcase.Spec) {
additionalTestBlkRan := testcase.LetValue(s, false)
s.Before(func(t *testcase.T) {
subject(t, func(it assert.It) { additionalTestBlkRan.Set(t, true) })
Expand All @@ -64,7 +64,7 @@ func TestAnyOf(t *testing.T) {
})
})

s.When(`.Test fails with .FailNow`, func(s *testcase.Spec) {
s.When(`.Case fails with .FailNow`, func(s *testcase.Spec) {
s.Before(func(t *testcase.T) {
subject(t, func(it assert.It) { it.Must.True(false) })
})
Expand Down Expand Up @@ -99,35 +99,35 @@ func TestAnyOf(t *testing.T) {
})
}

func TestAnyOf_Test_cleanup(t *testing.T) {
func TestA_Case_cleanup(t *testing.T) {
h := assert.Must(t)
stub := &doubles.TB{}
anyOf := &assert.AnyOf{
anyOf := &assert.A{
TB: stub,
Fail: stub.Fail,
}

var cleanupRan bool
anyOf.Test(func(it assert.It) {
anyOf.Case(func(it assert.It) {
it.Must.TB.Cleanup(func() { cleanupRan = true })
it.Must.True(false) // fail it
})
h.True(cleanupRan, "cleanup should have ran already after leaving the block of AnyOf.Test")
h.True(cleanupRan, "cleanup should have ran already after leaving the block of AnyOf.Case")

anyOf.Finish()
h.True(stub.IsFailed, "the provided testing.TB should have failed")
}

func TestAnyOf_Test_race(t *testing.T) {
stub := &doubles.TB{}
anyOf := &assert.AnyOf{
anyOf := &assert.A{
TB: stub,
Fail: stub.Fail,
}
testcase.Race(func() {
anyOf.Test(func(it assert.It) {})
anyOf.Case(func(it assert.It) {})
}, func() {
anyOf.Test(func(it assert.It) {})
anyOf.Case(func(it assert.It) {})
}, func() {
anyOf.Finish()
})
Expand Down
43 changes: 41 additions & 2 deletions assert/Asserter.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,9 +782,9 @@ func (a Asserter) containExactlySlice(exp reflect.Value, act reflect.Value, msg
}
}

func (a Asserter) AnyOf(blk func(a *AnyOf), msg ...Message) {
func (a Asserter) AnyOf(blk func(a *A), msg ...Message) {
a.TB.Helper()
anyOf := &AnyOf{TB: a.TB, Fail: a.Fail}
anyOf := &A{TB: a.TB, Fail: a.Fail}
defer anyOf.Finish(msg...)
blk(anyOf)
}
Expand Down Expand Up @@ -1029,6 +1029,7 @@ func (a Asserter) NotWithin(timeout time.Duration, blk func(context.Context), ms
}

func (a Asserter) within(timeout time.Duration, blk func(context.Context)) bool {
a.TB.Helper()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var done, isFailNow uint32
Expand Down Expand Up @@ -1063,3 +1064,41 @@ func (a Asserter) Eventually(durationOrCount any, blk func(it It)) {
}
retry.Assert(a.TB, blk)
}

var oneOfSupportedKinds = map[reflect.Kind]struct{}{
reflect.Slice: {},
reflect.Array: {},
}

// OneOf evaluates whether at least one element within the given values meets the conditions set in the assertion block.
func (a Asserter) OneOf(values any, blk /* func( */ any, msg ...Message) {
tb := a.TB
tb.Helper()

vs := reflect.ValueOf(values)
_, ok := oneOfSupportedKinds[vs.Kind()]
Must(tb).True(ok, Message(fmt.Sprintf("unexpected list value type: %s", vs.Kind().String())))

var fnErrMsg = Message(fmt.Sprintf("invalid function signature\n\nExpected:\nfunc(it assert.It, v %s)", vs.Type().Elem()))
fn := reflect.ValueOf(blk)
Must(tb).Equal(fn.Kind(), reflect.Func, "blk argument must be a function")
Must(tb).Equal(fn.Type().NumIn(), 2, fnErrMsg)
Must(tb).Equal(fn.Type().In(0), reflect.TypeOf((*It)(nil)).Elem(), fnErrMsg)
Must(tb).Equal(fn.Type().In(1), vs.Type().Elem(), fnErrMsg)

a.AnyOf(func(a *A) {
tb.Helper()
a.name = "OneOf"
a.cause = "None of the element matched the expectations"

for i := 0; i < vs.Len(); i++ {
e := vs.Index(i)
a.Case(func(it It) {
fn.Call([]reflect.Value{reflect.ValueOf(it), e})
})
if a.OK() {
break
}
}
}, msg...)
}
Loading

0 comments on commit dc2e612

Please sign in to comment.