Skip to content

Commit

Permalink
add assert.OneOf assertion helper
Browse files Browse the repository at this point in the history
  • Loading branch information
adamluzsi committed Mar 28, 2023
1 parent 62df428 commit 7fd8073
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 10 deletions.
20 changes: 17 additions & 3 deletions assert/AnyOf.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import (
"github.com/adamluzsi/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 ...any) {
Must(tb).AnyOf(func(a *AnyOf) {
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.
// common usage use-cases:
// - list of interface, where test order, or the underlying structure's implementation is irrelevant for the behavior.
Expand All @@ -30,7 +43,7 @@ type AnyOf struct {
// Using Test is safe for concurrently.
func (ao *AnyOf) Test(blk func(t It)) {
ao.TB.Helper()
if ao.isPassed() {
if ao.OK() {
return
}
recorder := &doubles.RecorderTB{TB: ao.TB}
Expand All @@ -48,12 +61,13 @@ func (ao *AnyOf) Test(blk func(t It)) {
ao.mutex.Lock()
defer ao.mutex.Unlock()
ao.passed = true
return
}

// Finish will check if any of the assertion succeeded.
func (ao *AnyOf) Finish(msg ...interface{}) {
ao.TB.Helper()
if ao.isPassed() {
if ao.OK() {
return
}
ao.TB.Log(fmterror.Message{
Expand All @@ -65,7 +79,7 @@ func (ao *AnyOf) Finish(msg ...interface{}) {
ao.Fail()
}

func (ao *AnyOf) isPassed() bool {
func (ao *AnyOf) OK() bool {
ao.mutex.Lock()
defer ao.mutex.Unlock()
return ao.passed
Expand Down
124 changes: 117 additions & 7 deletions assert/AnyOf_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package assert_test

import (
"github.com/adamluzsi/testcase/random"
"github.com/adamluzsi/testcase/sandbox"
"testing"

"github.com/adamluzsi/testcase"
Expand Down Expand Up @@ -31,12 +33,24 @@ func TestAnyOf(t *testing.T) {
t.Must.Equal(false, stub.Get(t).IsFailed)
})

s.Then("AnyOf.OK will be true, because one of the test passed", func(t *testcase.T) {
anyOf.Get(t).Finish()

t.Must.True(anyOf.Get(t).OK())
})

s.And(`and new .Test 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) })
})

s.Then("AnyOf.OK will be true, because one of the test passed", func(t *testcase.T) {
anyOf.Get(t).Finish()

t.Must.True(anyOf.Get(t).OK())
})

s.Then(`AnyOf yields no failure on .Finish`, func(t *testcase.T) {
anyOf.Get(t).Finish()
t.Must.Equal(false, stub.Get(t).IsFailed)
Expand All @@ -60,6 +74,12 @@ func TestAnyOf(t *testing.T) {
t.Must.True(true, stub.Get(t).IsFailed)
})

s.Then("AnyOf.OK will yield false due to no passing test", func(t *testcase.T) {
anyOf.Get(t).Finish()

t.Must.False(anyOf.Get(t).OK())
})

s.And(`but there is one as well that pass`, func(s *testcase.Spec) {
s.Before(func(t *testcase.T) {
subject(t, func(it assert.It) {})
Expand All @@ -69,6 +89,12 @@ func TestAnyOf(t *testing.T) {
anyOf.Get(t).Finish()
t.Must.Equal(false, stub.Get(t).IsFailed)
})

s.Then("AnyOf.OK will be true, because one of the test passed", func(t *testcase.T) {
anyOf.Get(t).Finish()

t.Must.True(anyOf.Get(t).OK())
})
})
})
}
Expand Down Expand Up @@ -107,10 +133,94 @@ func TestAnyOf_Test_race(t *testing.T) {
})
}

//func TestAnyOf_smoke(t *testing.T) {
// assert.Should(t).AnyOf(func(a *assert.AnyOf) {
// //a.Test(func(it assert.It) {})
// a.Test(func(it assert.It) {it.Must.True(false)})
// })
// t.Log(`after`)
//}
func TestOneOf(t *testing.T) {
s := testcase.NewSpec(t)

stub := testcase.Let(s, func(t *testcase.T) *doubles.TB {
return &doubles.TB{}
})
vs := testcase.Let(s, func(t *testcase.T) []string {
return random.Slice(t.Random.IntBetween(3, 7), func() string {
return t.Random.String()
})
})

const msg = "optional assertion explanation"
blk := testcase.LetValue[func(assert.It, string)](s, nil)
act := func(t *testcase.T) sandbox.RunOutcome {
return sandbox.Run(func() {
assert.OneOf(stub.Get(t), vs.Get(t), blk.Get(t), msg)
})
}

s.When("passed block has no issue", func(s *testcase.Spec) {
blk.Let(s, func(t *testcase.T) func(assert.It, string) {
return func(it assert.It, s string) {}
})

s.Then("testing.TB is OK", func(t *testcase.T) {
act(t)

t.Must.False(stub.Get(t).IsFailed)
})

s.Then("execution context is not killed", func(t *testcase.T) {
t.Must.True(act(t).OK)
})

s.Then("assert message explanation is not logged", func(t *testcase.T) {
act(t)

t.Must.NotContain(stub.Get(t).Logs.String(), msg)
})
})

s.When("passed keeps failing with testing.TB#FailNow", func(s *testcase.Spec) {
blk.Let(s, func(t *testcase.T) func(assert.It, string) {
return func(it assert.It, s string) { it.FailNow() }
})

s.Then("testing.TB is failed", func(t *testcase.T) {
act(t)

t.Must.True(stub.Get(t).IsFailed)
})

s.Then("execution context is interrupted with FailNow", func(t *testcase.T) {
out := act(t)
t.Must.False(out.OK)
t.Must.True(out.Goexit)
})

s.Then("assert message explanation is logged using the testing.TB", func(t *testcase.T) {
act(t)

t.Must.Contain(stub.Get(t).Logs.String(), msg)
})
})

s.When("assertion pass only for one of the slice element", func(s *testcase.Spec) {
blk.Let(s, func(t *testcase.T) func(assert.It, string) {
expected := t.Random.SliceElement(vs.Get(t)).(string)
return func(it assert.It, got string) {
it.Must.Equal(expected, got)
}
})

s.Then("testing.TB is OK", func(t *testcase.T) {
act(t)

t.Must.False(stub.Get(t).IsFailed)
})

s.Then("execution context is not killed", func(t *testcase.T) {
t.Must.True(act(t).OK)
})

s.Then("assert message explanation is not logged", func(t *testcase.T) {
act(t)

t.Must.NotContain(stub.Get(t).Logs.String(), msg)
})
})
}
17 changes: 17 additions & 0 deletions assert/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,14 @@ func ExampleWaiter_While() {
})
}

func ExampleEventuallyWithin() {
var tb testing.TB
assert.EventuallyWithin(5*time.Second).Assert(tb, func(it assert.It) {
// use "it" as you would tb, but if the test fails with "it"
// then the function block will be retried until the allowed time duration, which is one minute in this case.
})
}

func ExampleEventuallyWithin_byCount() {
var tb testing.TB
assert.EventuallyWithin(3 /* times */).Assert(tb, func(it assert.It) {
Expand Down Expand Up @@ -645,3 +653,12 @@ func ExampleAsserter_NotWithin() {
time.Sleep(time.Second) // OK
})
}

func ExampleOneOf() {
var tb testing.TB
values := []string{"foo", "bar", "baz"}

assert.OneOf(tb, values, func(it assert.It, got string) {
it.Must.Equal("bar", got)
}, "optional assertion explanation")
}

0 comments on commit 7fd8073

Please sign in to comment.