Skip to content

Commit e480ffe

Browse files
committed
better helpers
1 parent e105667 commit e480ffe

File tree

4 files changed

+273
-33
lines changed

4 files changed

+273
-33
lines changed

channels.go

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,118 @@
11
package helpers
22

33
import (
4+
"fmt"
45
"time"
56

67
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89
)
910

10-
// TryReceive returns the next value from the channel and true if successful, or returns an empty
11-
// value and false if the timeout expires first.
12-
func TryReceive[V any](ch <-chan V, timeout time.Duration) (V, bool) {
11+
// TryReceive waits for a value from the channel and returns (value, true, false) if
12+
// successful; (<empty>, false, false) if the timeout expired first; or
13+
// (<empty>, false, true) if the channel was closed.
14+
func TryReceive[V any](ch <-chan V, timeout time.Duration) (V, bool, bool) {
15+
deadline := time.NewTimer(timeout)
16+
defer deadline.Stop()
1317
select {
14-
case v := <-ch:
15-
return v, true
16-
case <-time.After(timeout):
18+
case v, ok := <-ch:
19+
if ok {
20+
return v, true, false
21+
}
22+
return v, false, true
23+
case <-deadline.C:
1724
var empty V
18-
return empty, false
25+
return empty, false, false
1926
}
2027
}
2128

2229
// RequireValue returns the next value from the channel, or forces an immediate test failure
2330
// and exit if the timeout expires first.
24-
func RequireValue[V any](t require.TestingT, ch <-chan V, timeout time.Duration) V {
25-
if v, ok := TryReceive(ch, timeout); ok {
31+
func RequireValue[V any](t require.TestingT, ch <-chan V, timeout time.Duration, customMessageAndArgs ...any) V {
32+
v, ok, closed := TryReceive(ch, timeout)
33+
if ok {
2634
return v
2735
}
2836
var empty V
29-
t.Errorf("expected a %T value from channel but did not receive one in %s", empty, timeout)
37+
if closed {
38+
failWithMessageAndArgs(t, customMessageAndArgs,
39+
"expected a %T value from channel but the channel was closed", empty)
40+
} else {
41+
failWithMessageAndArgs(t, customMessageAndArgs,
42+
"expected a %T value from channel but did not receive one in %s", empty, timeout)
43+
}
3044
t.FailNow()
3145
return empty // never reached
3246
}
3347

34-
// AssertNoMoreValues asserts that no value is available from the channel within the timeout.
35-
func AssertNoMoreValues[V any](t assert.TestingT, ch <-chan V, timeout time.Duration) bool {
36-
if v, ok := TryReceive(ch, timeout); ok {
37-
t.Errorf("expected no more %T values from channel but got one: %+v", v, v)
48+
// AssertNoMoreValues asserts that no value is available from the channel within the timeout,
49+
// but that the channel was not closed.
50+
func AssertNoMoreValues[V any](
51+
t assert.TestingT,
52+
ch <-chan V,
53+
timeout time.Duration,
54+
customMessageAndArgs ...any,
55+
) bool {
56+
v, ok, closed := TryReceive(ch, timeout)
57+
if ok {
58+
failWithMessageAndArgs(t, customMessageAndArgs,
59+
"expected no more %T values from channel but got one: %+v", v, v)
60+
return false
61+
}
62+
if closed {
63+
failWithMessageAndArgs(t, customMessageAndArgs, "channel was unexpectedly closed")
3864
return false
3965
}
4066
return true
4167
}
4268

43-
// RequireNoMoreValues is equivalent to AssertNoMoreValues except that it forces an immediate
44-
// test exit on failure.
45-
func RequireNoMoreValues[V any](t require.TestingT, ch <-chan V, timeout time.Duration) {
46-
if !AssertNoMoreValues(t, ch, timeout) {
47-
t.FailNow()
69+
// AssertChannelClosed asserts that the channel is closed within the timeout, sending no values.
70+
func AssertChannelClosed[V any](
71+
t assert.TestingT,
72+
ch <-chan V,
73+
timeout time.Duration,
74+
customMessageAndArgs ...any,
75+
) bool {
76+
v, ok, closed := TryReceive(ch, timeout)
77+
if ok {
78+
failWithMessageAndArgs(t, customMessageAndArgs,
79+
"expected no more %T values from channel but got one: %+v", v, v)
80+
return false
81+
}
82+
if !closed {
83+
failWithMessageAndArgs(t, customMessageAndArgs,
84+
"expected channel to be closed within %s but it was not", timeout)
85+
return false
86+
}
87+
return true
88+
}
89+
90+
// AssertChannelNotClosed asserts that the channel is not closed within the timeout, consuming
91+
// any values that may be sent during that time.
92+
func AssertChannelNotClosed[V any](
93+
t assert.TestingT,
94+
ch <-chan V,
95+
timeout time.Duration,
96+
customMessageAndArgs ...any,
97+
) bool {
98+
deadline := time.NewTimer(timeout)
99+
defer deadline.Stop()
100+
for {
101+
select {
102+
case _, ok := <-ch:
103+
if !ok {
104+
failWithMessageAndArgs(t, customMessageAndArgs, "channel was unexpectedly closed")
105+
return false
106+
}
107+
case <-deadline.C:
108+
return true
109+
}
110+
}
111+
}
112+
113+
func failWithMessageAndArgs(t assert.TestingT, customMessageAndArgs []any, defaultMsg string, defaultArgs ...any) {
114+
t.Errorf(defaultMsg, defaultArgs...)
115+
if len(customMessageAndArgs) != 0 {
116+
t.Errorf(fmt.Sprintf("%s", customMessageAndArgs[0]), customMessageAndArgs[1:]...)
48117
}
49118
}

channels_test.go

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,162 @@ import (
55
"time"
66

77
"github.com/launchdarkly/go-test-helpers/v2/testbox"
8+
89
"github.com/stretchr/testify/assert"
910
)
1011

1112
func TestTryReceive(t *testing.T) {
1213
ch := make(chan string, 1)
13-
v, ok := TryReceive(ch, time.Millisecond)
14+
v, ok, closed := TryReceive(ch, time.Millisecond)
1415
assert.False(t, ok)
16+
assert.False(t, closed)
1517
assert.Equal(t, "", v)
1618

1719
ch <- "a"
18-
v, ok = TryReceive(ch, time.Millisecond)
20+
v, ok, closed = TryReceive(ch, time.Millisecond)
1921
assert.True(t, ok)
22+
assert.False(t, closed)
2023
assert.Equal(t, "a", v)
24+
25+
go func() {
26+
close(ch)
27+
}()
28+
v, ok, closed = TryReceive(ch, time.Second)
29+
assert.False(t, ok)
30+
assert.True(t, closed)
31+
assert.Equal(t, "", v)
2132
}
2233

2334
func TestRequireValue(t *testing.T) {
24-
result := testbox.SandboxTest(func(t1 testbox.TestingT) {
35+
testbox.ShouldFailAndExitEarly(t, func(t testbox.TestingT) {
2536
ch := make(chan string, 1)
26-
_ = RequireValue(t1, ch, time.Millisecond)
27-
t.Errorf("test should have exited early but did not")
37+
_ = RequireValue(t, ch, time.Millisecond)
2838
})
29-
assert.True(t, result.Failed)
3039

3140
ch := make(chan string, 1)
3241
go func() {
3342
ch <- "a"
3443
}()
3544
v := RequireValue(t, ch, time.Second)
3645
assert.Equal(t, "a", v)
46+
47+
testbox.ShouldFailAndExitEarly(t, func(t testbox.TestingT) {
48+
ch := make(chan string, 1)
49+
go func() {
50+
close(ch)
51+
}()
52+
_ = RequireValue(t, ch, time.Second)
53+
})
3754
}
3855

3956
func TestAssertNoMoreValues(t *testing.T) {
4057
ch := make(chan string, 1)
4158
AssertNoMoreValues(t, ch, time.Millisecond)
4259

43-
result := testbox.SandboxTest(func(t testbox.TestingT) {
60+
testbox.ShouldFail(t, func(t testbox.TestingT) {
4461
ch := make(chan string, 1)
4562
go func() {
4663
ch <- "a"
4764
}()
4865
AssertNoMoreValues(t, ch, time.Second)
4966
})
50-
assert.True(t, result.Failed)
67+
68+
testbox.ShouldFail(t, func(t testbox.TestingT) {
69+
ch := make(chan string, 1)
70+
go func() {
71+
close(ch)
72+
}()
73+
AssertNoMoreValues(t, ch, time.Second)
74+
})
5175
}
5276

53-
func TestRequireNoMoreValues(t *testing.T) {
77+
func TestAssertChannelClosed(t *testing.T) {
5478
ch := make(chan string, 1)
55-
AssertNoMoreValues(t, ch, time.Millisecond)
79+
go func() {
80+
close(ch)
81+
}()
82+
AssertChannelClosed(t, ch, time.Second)
83+
84+
testbox.ShouldFail(t, func(t testbox.TestingT) {
85+
ch := make(chan string, 1)
86+
AssertChannelClosed(t, ch, time.Millisecond)
87+
})
88+
89+
testbox.ShouldFail(t, func(t testbox.TestingT) {
90+
ch := make(chan string, 1)
91+
ch <- "a"
92+
AssertChannelClosed(t, ch, time.Millisecond)
93+
})
94+
}
95+
96+
func TestAssertChannelNotClosed(t *testing.T) {
97+
testbox.ShouldFail(t, func(t testbox.TestingT) {
98+
ch := make(chan string, 1)
99+
go func() {
100+
close(ch)
101+
}()
102+
AssertChannelNotClosed(t, ch, time.Second)
103+
})
104+
105+
ch := make(chan string, 1)
106+
AssertChannelNotClosed(t, ch, time.Millisecond)
107+
108+
ch <- "a"
109+
AssertChannelNotClosed(t, ch, time.Millisecond)
110+
}
111+
112+
func TestFailureMessages(t *testing.T) {
113+
result := testbox.SandboxTest(func(t testbox.TestingT) {
114+
ch := make(chan string, 1)
115+
_ = RequireValue(t, ch, time.Millisecond, "sorry%s", ".")
116+
})
117+
if assert.Len(t, result.Failures, 2) {
118+
assert.Equal(t, "expected a string value from channel but did not receive one in 1ms", result.Failures[0].Message)
119+
assert.Equal(t, "sorry.", result.Failures[1].Message)
120+
}
56121

57-
result := testbox.SandboxTest(func(t1 testbox.TestingT) {
122+
result = testbox.SandboxTest(func(t testbox.TestingT) {
58123
ch := make(chan string, 1)
59124
go func() {
60125
ch <- "a"
61126
}()
62-
RequireNoMoreValues(t1, ch, time.Second)
63-
t.Errorf("test should have exited early but did not")
127+
AssertNoMoreValues(t, ch, time.Second, "sorry%s", ".")
128+
})
129+
if assert.Len(t, result.Failures, 2) {
130+
assert.Equal(t, "expected no more string values from channel but got one: a", result.Failures[0].Message)
131+
assert.Equal(t, "sorry.", result.Failures[1].Message)
132+
}
133+
134+
result = testbox.SandboxTest(func(t testbox.TestingT) {
135+
ch := make(chan string, 1)
136+
go func() {
137+
close(ch)
138+
}()
139+
AssertNoMoreValues(t, ch, time.Second, "sorry%s", ".")
140+
})
141+
if assert.Len(t, result.Failures, 2) {
142+
assert.Equal(t, "channel was unexpectedly closed", result.Failures[0].Message)
143+
assert.Equal(t, "sorry.", result.Failures[1].Message)
144+
}
145+
146+
result = testbox.SandboxTest(func(t testbox.TestingT) {
147+
ch := make(chan string, 1)
148+
AssertChannelClosed(t, ch, time.Millisecond, "sorry%s", ".")
149+
})
150+
if assert.Len(t, result.Failures, 2) {
151+
assert.Equal(t, "expected channel to be closed within 1ms but it was not", result.Failures[0].Message)
152+
assert.Equal(t, "sorry.", result.Failures[1].Message)
153+
}
154+
155+
result = testbox.SandboxTest(func(t testbox.TestingT) {
156+
ch := make(chan string, 1)
157+
go func() {
158+
close(ch)
159+
}()
160+
AssertChannelNotClosed(t, ch, time.Second, "sorry%s", ".")
64161
})
65-
assert.True(t, result.Failed)
162+
if assert.Len(t, result.Failures, 2) {
163+
assert.Equal(t, "channel was unexpectedly closed", result.Failures[0].Message)
164+
assert.Equal(t, "sorry.", result.Failures[1].Message)
165+
}
66166
}

testbox/sandbox.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"runtime"
66
"strings"
77
"sync"
8+
9+
"github.com/stretchr/testify/assert"
810
)
911

1012
// SandboxResult describes the aggregate test state produced by calling SandboxTest.
@@ -148,3 +150,41 @@ func (m *mockTestingT) runSafely(action func(TestingT)) {
148150
}()
149151
<-exited
150152
}
153+
154+
// ShouldFail is a shortcut for running some action against a testbox.TestingT and
155+
// asserting that it failed.
156+
func ShouldFail(t assert.TestingT, action func(TestingT)) bool {
157+
shouldGetHere := make(chan struct{}, 1)
158+
result := SandboxTest(func(t1 TestingT) {
159+
action(t1)
160+
shouldGetHere <- struct{}{}
161+
})
162+
if !result.Failed {
163+
t.Errorf("expected test to fail, but it passed")
164+
return false
165+
}
166+
if len(shouldGetHere) == 0 {
167+
t.Errorf("test failed as expected, but it also terminated early and should not have")
168+
return false
169+
}
170+
return true
171+
}
172+
173+
// ShouldFailAndExitEarly is the same as ShouldFail, except that it also asserts that
174+
// the test was terminated early with FailNow.
175+
func ShouldFailAndExitEarly(t assert.TestingT, action func(TestingT)) bool {
176+
shouldNotGetHere := make(chan struct{}, 1)
177+
result := SandboxTest(func(t1 TestingT) {
178+
action(t1)
179+
shouldNotGetHere <- struct{}{}
180+
})
181+
if !result.Failed {
182+
t.Errorf("expected test to fail, but it passed")
183+
return false
184+
}
185+
if len(shouldNotGetHere) != 0 {
186+
t.Errorf("test failed as expected, but it should have also terminated early and did not")
187+
return false
188+
}
189+
return true
190+
}

0 commit comments

Comments
 (0)