Skip to content

Commit 0f3436d

Browse files
authored
feat: add CheckedClose test helper (#27217)
* feat: add CheckedClose test helper Add CheckedClose test helper to improve readability of the following pattern in tests. ``` defer func() { require.NoError(t, c.Close()) }() ``` This can now be replaced with `defer CheckedClose(t, c)()`. * feat: add CheckedCloseOnce Add CheckedCloseOnce test helper function to create close functions that check for errors and only call the underlying Close method once. Clean chery-pick from master-1.x. (cherry picked from commit 093bd3f)
1 parent 049b1f7 commit 0f3436d

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

pkg/testing/helper/helpers.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package helper
2+
3+
import (
4+
"io"
5+
"sync"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// CheckedClose returns a function that can be used with a defer to check that a
11+
// Closer's Close method does not return an error.
12+
//
13+
// By using CheckedClose, the following code:
14+
// defer func() { require.NoError(t, c.Close()) }()
15+
// can be replaced by:
16+
// defer CheckedClose(t, c)()
17+
func CheckedClose(t require.TestingT, c io.Closer) func() {
18+
return func() {
19+
if h, ok := t.(interface{ Helper() }); ok {
20+
h.Helper()
21+
}
22+
23+
require.NoError(t, c.Close())
24+
}
25+
}
26+
27+
// CheckedCloseOnce returns a function that will call Close on c and check
28+
// for errors. The returned function has no effect after being called the first
29+
// time. It can be used to replace code like the following:
30+
//
31+
// c := NewSomeCloser()
32+
// defer func() {
33+
// if c != nil {
34+
// require.NoError(c.Close())
35+
// c = nil
36+
// }
37+
// }()
38+
// ...
39+
// require.NoError(t, c.Close())
40+
// c = nil
41+
//
42+
// Example:
43+
//
44+
// closer := CheckedCloseOnce(t, c)
45+
// defer closer()
46+
// ...
47+
// closer()
48+
func CheckedCloseOnce(t require.TestingT, c io.Closer) func() {
49+
innerCloser := CheckedClose(t, c)
50+
var o sync.Once
51+
return func() {
52+
o.Do(innerCloser)
53+
}
54+
}

pkg/testing/helper/helpers_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package helper
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// mockCloser is an io.Closer that returns a configurable error.
11+
type mockCloser struct {
12+
err error
13+
closed bool
14+
closeCount int
15+
}
16+
17+
func (m *mockCloser) Close() error {
18+
m.closed = true
19+
m.closeCount++
20+
return m.err
21+
}
22+
23+
// mockTestingT implements require.TestingT for testing CheckedClose behavior.
24+
type mockTestingT struct {
25+
failed bool
26+
helperUsed bool
27+
}
28+
29+
func (m *mockTestingT) Errorf(format string, args ...any) {
30+
m.failed = true
31+
}
32+
33+
func (m *mockTestingT) FailNow() {
34+
m.failed = true
35+
}
36+
37+
func (m *mockTestingT) Helper() {
38+
m.helperUsed = true
39+
}
40+
41+
func TestCheckedClose(t *testing.T) {
42+
t.Run("returns a function", func(t *testing.T) {
43+
closer := &mockCloser{}
44+
fn := CheckedClose(t, closer)
45+
require.NotNil(t, fn)
46+
})
47+
48+
t.Run("calls Close on the closer", func(t *testing.T) {
49+
closer := &mockCloser{}
50+
fn := CheckedClose(t, closer)
51+
fn()
52+
require.True(t, closer.closed)
53+
})
54+
55+
t.Run("does not fail when Close returns nil", func(t *testing.T) {
56+
mockT := &mockTestingT{}
57+
closer := &mockCloser{err: nil}
58+
fn := CheckedClose(mockT, closer)
59+
fn()
60+
require.False(t, mockT.failed)
61+
})
62+
63+
t.Run("fails when Close returns an error", func(t *testing.T) {
64+
mockT := &mockTestingT{}
65+
closer := &mockCloser{err: errors.New("close error")}
66+
fn := CheckedClose(mockT, closer)
67+
fn()
68+
require.True(t, mockT.failed)
69+
})
70+
71+
t.Run("calls Helper when available", func(t *testing.T) {
72+
mockT := &mockTestingT{}
73+
closer := &mockCloser{}
74+
fn := CheckedClose(mockT, closer)
75+
fn()
76+
require.True(t, mockT.helperUsed)
77+
})
78+
79+
t.Run("works with defer", func(t *testing.T) {
80+
closer := &mockCloser{}
81+
func() {
82+
defer CheckedClose(t, closer)()
83+
require.False(t, closer.closed, "should not be closed yet")
84+
}()
85+
require.True(t, closer.closed, "should be closed after function returns")
86+
})
87+
}
88+
89+
func TestCheckedCloseOnce(t *testing.T) {
90+
t.Run("returns a function", func(t *testing.T) {
91+
closer := &mockCloser{}
92+
fn := CheckedCloseOnce(t, closer)
93+
require.NotNil(t, fn)
94+
})
95+
96+
t.Run("calls Close on the closer", func(t *testing.T) {
97+
closer := &mockCloser{}
98+
fn := CheckedCloseOnce(t, closer)
99+
fn()
100+
require.True(t, closer.closed)
101+
})
102+
103+
t.Run("does not fail when Close returns nil", func(t *testing.T) {
104+
mockT := &mockTestingT{}
105+
closer := &mockCloser{err: nil}
106+
fn := CheckedCloseOnce(mockT, closer)
107+
fn()
108+
require.False(t, mockT.failed)
109+
})
110+
111+
t.Run("fails when Close returns an error", func(t *testing.T) {
112+
mockT := &mockTestingT{}
113+
closer := &mockCloser{err: errors.New("close error")}
114+
fn := CheckedCloseOnce(mockT, closer)
115+
fn()
116+
require.True(t, mockT.failed)
117+
})
118+
119+
t.Run("calls Helper when available", func(t *testing.T) {
120+
mockT := &mockTestingT{}
121+
closer := &mockCloser{}
122+
fn := CheckedCloseOnce(mockT, closer)
123+
fn()
124+
require.True(t, mockT.helperUsed)
125+
})
126+
127+
t.Run("works with defer", func(t *testing.T) {
128+
closer := &mockCloser{}
129+
func() {
130+
defer CheckedCloseOnce(t, closer)()
131+
require.False(t, closer.closed, "should not be closed yet")
132+
}()
133+
require.True(t, closer.closed, "should be closed after function returns")
134+
})
135+
136+
t.Run("only closes once when called multiple times", func(t *testing.T) {
137+
closer := &mockCloser{}
138+
fn := CheckedCloseOnce(t, closer)
139+
fn()
140+
fn()
141+
fn()
142+
require.Equal(t, 1, closer.closeCount)
143+
})
144+
145+
t.Run("works with defer and explicit call", func(t *testing.T) {
146+
closer := &mockCloser{}
147+
func() {
148+
closeFn := CheckedCloseOnce(t, closer)
149+
defer closeFn()
150+
closeFn() // explicit early close
151+
require.Equal(t, 1, closer.closeCount, "should be closed exactly once")
152+
}()
153+
require.Equal(t, 1, closer.closeCount, "should still be closed exactly once after defer")
154+
})
155+
}

0 commit comments

Comments
 (0)