Skip to content

Commit bae643d

Browse files
Smeromonsi
authored andcommitted
add matcher relecting errors.Is behavior
1 parent a3ca2ca commit bae643d

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

matchers.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,24 @@ func MatchError(expected any, functionErrorDescription ...any) types.GomegaMatch
146146
}
147147
}
148148

149+
// MatchErrorStrictly succeeds iff actual is a non-nil error that matches the passed in
150+
// expected error according to errors.Is(actual, expected).
151+
//
152+
// This behavior differs from MatchError where
153+
//
154+
// Expect(errors.New("some error")).To(MatchError(errors.New("some error")))
155+
//
156+
// succeeds, but errors.Is would return false so:
157+
//
158+
// Expect(errors.New("some error")).To(MatchErrorStrictly(errors.New("some error")))
159+
//
160+
// fails.
161+
func MatchErrorStrictly(expected error) types.GomegaMatcher {
162+
return &matchers.MatchErrorStrictlyMatcher{
163+
Expected: expected,
164+
}
165+
}
166+
149167
// BeClosed succeeds if actual is a closed channel.
150168
// It is an error to pass a non-channel to BeClosed, it is also an error to pass nil
151169
//
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package matchers
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/onsi/gomega/format"
8+
)
9+
10+
type MatchErrorStrictlyMatcher struct {
11+
Expected error
12+
}
13+
14+
func (matcher *MatchErrorStrictlyMatcher) Match(actual any) (success bool, err error) {
15+
16+
if isNil(matcher.Expected) {
17+
return false, fmt.Errorf("Expected error is nil, use \"ToNot(HaveOccurred())\" to explicitly check for nil errors")
18+
}
19+
20+
if isNil(actual) {
21+
return false, fmt.Errorf("Expected an error, got nil")
22+
}
23+
24+
if !isError(actual) {
25+
return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1))
26+
}
27+
28+
actualErr := actual.(error)
29+
30+
return errors.Is(actualErr, matcher.Expected), nil
31+
}
32+
33+
func (matcher *MatchErrorStrictlyMatcher) FailureMessage(actual any) (message string) {
34+
return format.Message(actual, "to match error", matcher.Expected)
35+
}
36+
37+
func (matcher *MatchErrorStrictlyMatcher) NegatedFailureMessage(actual any) (message string) {
38+
return format.Message(actual, "not to match error", matcher.Expected)
39+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package matchers_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
. "github.com/onsi/gomega/matchers"
10+
)
11+
12+
type FakeIsError struct {
13+
isError bool
14+
}
15+
16+
func (f *FakeIsError) Error() string {
17+
return fmt.Sprintf("is other error: %T", f.isError)
18+
}
19+
20+
func (f *FakeIsError) Is(other error) bool {
21+
return f.isError
22+
}
23+
24+
var _ = Describe("MatchErrorStrictlyMatcher", func() {
25+
Context("When asserting against an error", func() {
26+
When("passed an error", func() {
27+
It("should succeed when errors.Is returns true", func() {
28+
err := errors.New("an error")
29+
fmtErr := fmt.Errorf("an error")
30+
isError := &FakeIsError{true}
31+
32+
Expect(err).To(MatchErrorStrictly(err))
33+
Expect(fmtErr).To(MatchErrorStrictly(fmtErr))
34+
Expect(isError).To(MatchErrorStrictly(errors.New("any error should match")))
35+
})
36+
37+
It("should fail when errors.Is returns false", func() {
38+
err := errors.New("an error")
39+
fmtErr := fmt.Errorf("an error")
40+
isNotError := &FakeIsError{false}
41+
42+
Expect(err).ToNot(MatchErrorStrictly(errors.New("another error")))
43+
Expect(fmtErr).ToNot(MatchErrorStrictly(fmt.Errorf("an error")))
44+
45+
// errors.Is first checks if the values equal via ==, so we must point
46+
// to different instances of otherwise equal FakeIsError
47+
Expect(isNotError).ToNot(MatchErrorStrictly(&FakeIsError{false}))
48+
})
49+
50+
It("should succeed when any error in the chain matches the passed error", func() {
51+
innerErr := errors.New("inner error")
52+
outerErr := fmt.Errorf("outer error wrapping: %w", innerErr)
53+
54+
Expect(outerErr).To(MatchErrorStrictly(innerErr))
55+
})
56+
})
57+
})
58+
59+
When("expected is nil", func() {
60+
It("should fail with an appropriate error", func() {
61+
_, err := (&MatchErrorStrictlyMatcher{
62+
Expected: nil,
63+
}).Match(errors.New("an error"))
64+
Expect(err).To(HaveOccurred())
65+
Expect(err.Error()).To(ContainSubstring("ToNot(HaveOccurred())"))
66+
})
67+
})
68+
69+
When("passed nil", func() {
70+
It("should fail", func() {
71+
_, err := (&MatchErrorStrictlyMatcher{
72+
Expected: errors.New("an error"),
73+
}).Match(nil)
74+
Expect(err).To(HaveOccurred())
75+
})
76+
})
77+
78+
When("passed a non-error", func() {
79+
It("should fail", func() {
80+
_, err := (&MatchErrorStrictlyMatcher{
81+
Expected: errors.New("an error"),
82+
}).Match("an error")
83+
Expect(err).To(HaveOccurred())
84+
85+
_, err = (&MatchErrorStrictlyMatcher{
86+
Expected: errors.New("an error"),
87+
}).Match(3)
88+
Expect(err).To(HaveOccurred())
89+
})
90+
})
91+
92+
It("shows failure message", func() {
93+
failuresMessages := InterceptGomegaFailures(func() {
94+
Expect(errors.New("foo")).To(MatchErrorStrictly(errors.New("bar")))
95+
})
96+
Expect(failuresMessages[0]).To(ContainSubstring("foo\n {s: \"foo\"}\nto match error\n <*errors.errorString"))
97+
})
98+
99+
It("shows negated failure message", func() {
100+
err := errors.New("foo")
101+
failuresMessages := InterceptGomegaFailures(func() {
102+
Expect(err).ToNot(MatchErrorStrictly(err))
103+
})
104+
Expect(failuresMessages[0]).To(ContainSubstring("foo\n {s: \"foo\"}\nnot to match error\n <*errors.errorString"))
105+
})
106+
107+
})

0 commit comments

Comments
 (0)