Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ func MatchError(expected any, functionErrorDescription ...any) types.GomegaMatch
}
}

// MatchErrorStrictly succeeds iff actual is a non-nil error that matches the passed in
// expected error according to errors.Is(actual, expected).
//
// This behavior differs from MatchError where
//
// Expect(errors.New("some error")).To(MatchError(errors.New("some error")))
//
// succeeds, but errors.Is would return false so:
//
// Expect(errors.New("some error")).To(MatchErrorStrictly(errors.New("some error")))
//
// fails.
func MatchErrorStrictly(expected error) types.GomegaMatcher {
return &matchers.MatchErrorStrictlyMatcher{
Expected: expected,
}
}

// BeClosed succeeds if actual is a closed channel.
// It is an error to pass a non-channel to BeClosed, it is also an error to pass nil
//
Expand Down
39 changes: 39 additions & 0 deletions matchers/match_error_strictly_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package matchers

import (
"errors"
"fmt"

"github.com/onsi/gomega/format"
)

type MatchErrorStrictlyMatcher struct {
Expected error
}

func (matcher *MatchErrorStrictlyMatcher) Match(actual any) (success bool, err error) {

if isNil(matcher.Expected) {
return false, fmt.Errorf("Expected error is nil, use \"ToNot(HaveOccurred())\" to explicitly check for nil errors")
}

if isNil(actual) {
return false, fmt.Errorf("Expected an error, got nil")
}

if !isError(actual) {
return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1))
}

actualErr := actual.(error)

return errors.Is(actualErr, matcher.Expected), nil
}

func (matcher *MatchErrorStrictlyMatcher) FailureMessage(actual any) (message string) {
return format.Message(actual, "to match error", matcher.Expected)
}

func (matcher *MatchErrorStrictlyMatcher) NegatedFailureMessage(actual any) (message string) {
return format.Message(actual, "not to match error", matcher.Expected)
}
107 changes: 107 additions & 0 deletions matchers/match_error_strictly_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package matchers_test

import (
"errors"
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/matchers"
)

type FakeIsError struct {
isError bool
}

func (f *FakeIsError) Error() string {
return fmt.Sprintf("is other error: %T", f.isError)
}

func (f *FakeIsError) Is(other error) bool {
return f.isError
}

var _ = Describe("MatchErrorStrictlyMatcher", func() {
Context("When asserting against an error", func() {
When("passed an error", func() {
It("should succeed when errors.Is returns true", func() {
err := errors.New("an error")
fmtErr := fmt.Errorf("an error")
isError := &FakeIsError{true}

Expect(err).To(MatchErrorStrictly(err))
Expect(fmtErr).To(MatchErrorStrictly(fmtErr))
Expect(isError).To(MatchErrorStrictly(errors.New("any error should match")))
})

It("should fail when errors.Is returns false", func() {
err := errors.New("an error")
fmtErr := fmt.Errorf("an error")
isNotError := &FakeIsError{false}

Expect(err).ToNot(MatchErrorStrictly(errors.New("another error")))
Expect(fmtErr).ToNot(MatchErrorStrictly(fmt.Errorf("an error")))

// errors.Is first checks if the values equal via ==, so we must point
// to different instances of otherwise equal FakeIsError
Expect(isNotError).ToNot(MatchErrorStrictly(&FakeIsError{false}))
})

It("should succeed when any error in the chain matches the passed error", func() {
innerErr := errors.New("inner error")
outerErr := fmt.Errorf("outer error wrapping: %w", innerErr)

Expect(outerErr).To(MatchErrorStrictly(innerErr))
})
})
})

When("expected is nil", func() {
It("should fail with an appropriate error", func() {
_, err := (&MatchErrorStrictlyMatcher{
Expected: nil,
}).Match(errors.New("an error"))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("ToNot(HaveOccurred())"))
})
})

When("passed nil", func() {
It("should fail", func() {
_, err := (&MatchErrorStrictlyMatcher{
Expected: errors.New("an error"),
}).Match(nil)
Expect(err).To(HaveOccurred())
})
})

When("passed a non-error", func() {
It("should fail", func() {
_, err := (&MatchErrorStrictlyMatcher{
Expected: errors.New("an error"),
}).Match("an error")
Expect(err).To(HaveOccurred())

_, err = (&MatchErrorStrictlyMatcher{
Expected: errors.New("an error"),
}).Match(3)
Expect(err).To(HaveOccurred())
})
})

It("shows failure message", func() {
failuresMessages := InterceptGomegaFailures(func() {
Expect(errors.New("foo")).To(MatchErrorStrictly(errors.New("bar")))
})
Expect(failuresMessages[0]).To(ContainSubstring("foo\n {s: \"foo\"}\nto match error\n <*errors.errorString"))
})

It("shows negated failure message", func() {
err := errors.New("foo")
failuresMessages := InterceptGomegaFailures(func() {
Expect(err).ToNot(MatchErrorStrictly(err))
})
Expect(failuresMessages[0]).To(ContainSubstring("foo\n {s: \"foo\"}\nnot to match error\n <*errors.errorString"))
})

})