Added Add, Or, Not, WithTransform matchers, for composability.
- Allows matchers to be composed into complex expressions that work even with the Eventually() assertion.
- Also makes it easy to create new matchers -- can often write a function that composes a new matcher out of existing ones.
diff --git a/matchers.go b/matchers.go
index 3b39677..872de9e 100644
--- a/matchers.go
+++ b/matchers.go
@@ -343,3 +343,41 @@
func BeADirectory() types.GomegaMatcher {
return &matchers.BeADirectoryMatcher{}
}
+
+//And succeeds only if all of the given matchers succeed.
+//The matchers are tried in order, and will fail-fast if one doesn't succeed.
+// Expect("hi").To(And(HaveLen(2), Equal("hi"))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func And(ms ...types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.AndMatcher{Matchers: ms}
+}
+
+//Or succeeds if any of the given matchers succeed.
+//The matchers are tried in order and will return immediately upon the first successful match.
+// Expect("hi").To(Or(HaveLen(3), HaveLen(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Or(ms ...types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.OrMatcher{Matchers: ms}
+}
+
+//Not negates the given matcher; it succeeds if the given matcher fails.
+// Expect(1).To(Not(Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Not(matcher types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.NotMatcher{Matcher: matcher}
+}
+
+//WithTransform applies the `transform` to the actual value and matches it against `matcher`.
+// var plus1 = func(i interface{}) interface{} { return i.(int) + 1 }
+// Expect(1).To(WithTransform(plus1, Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func WithTransform(transform func(interface{}) interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.WithTransformMatcher{
+ Transform: transform,
+ Matcher: matcher,
+ }
+}
diff --git a/matchers/and.go b/matchers/and.go
new file mode 100644
index 0000000..e20fbae
--- /dev/null
+++ b/matchers/and.go
@@ -0,0 +1,34 @@
+package matchers
+
+import (
+ "fmt"
+ "github.com/onsi/gomega/format"
+ "github.com/onsi/gomega/types"
+)
+
+type AndMatcher struct {
+ Matchers []types.GomegaMatcher
+
+ // state
+ firstFailedMatchErrMsg string
+}
+
+func (m *AndMatcher) Match(actual interface{}) (success bool, err error) {
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if !success || err != nil {
+ m.firstFailedMatchErrMsg = matcher.FailureMessage(actual)
+ return false, err
+ }
+ }
+ return true, nil
+}
+
+func (m *AndMatcher) FailureMessage(actual interface{}) (message string) {
+ return m.firstFailedMatchErrMsg
+}
+
+func (m *AndMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ // not the most beautiful list of matchers, but not bad either...
+ return format.Message(actual, fmt.Sprintf("To not satisfy all of these matchers: %s", m.Matchers))
+}
diff --git a/matchers/and_test.go b/matchers/and_test.go
new file mode 100644
index 0000000..c50e800
--- /dev/null
+++ b/matchers/and_test.go
@@ -0,0 +1,61 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/onsi/gomega/types"
+)
+
+// sample data
+var (
+ // example input
+ input = "hi"
+ // some matchers that succeed against the input
+ true1 = HaveLen(2)
+ true2 = Equal("hi")
+ true3 = MatchRegexp("hi")
+ // some matchers that fail against the input.
+ false1 = HaveLen(1)
+ false2 = Equal("hip")
+ false3 = MatchRegexp("hope")
+)
+
+// verifyFailureMessage expects the matcher to fail with the given input, and verifies the failure message.
+func verifyFailureMessage(m types.GomegaMatcher, input string, expectedFailureMsgFragment string) {
+ Expect(m.Match(input)).To(BeFalse())
+ Expect(m.FailureMessage(input)).To(Equal(
+ "Expected\n <string>: " + input + "\n" + expectedFailureMsgFragment))
+}
+
+var _ = Describe("AndMatcher", func() {
+ It("works with positive cases", func() {
+ Expect(input).To(And())
+ Expect(input).To(And(true1))
+ Expect(input).To(And(true1, true2))
+ Expect(input).To(And(true1, true2, true3))
+ })
+
+ It("works with negative cases", func() {
+ Expect(input).ToNot(And(false1, false2))
+ Expect(input).ToNot(And(true1, true2, false3))
+ Expect(input).ToNot(And(true1, false2, false3))
+ Expect(input).ToNot(And(false1, true1, true2))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(And(false1, true1), input, "to have length 1")
+ verifyFailureMessage(And(true1, false2), input, "to equal\n <string>: hip")
+ verifyFailureMessage(And(true1, true2, false3), input, "to match regular expression\n <string>: hope")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(And(true1, true2)), input,
+ `To not satisfy all of these matchers: [%!s(*matchers.HaveLenMatcher=&{2}) %!s(*matchers.EqualMatcher=&{hi})]`)
+ })
+ })
+ })
+})
diff --git a/matchers/not.go b/matchers/not.go
new file mode 100644
index 0000000..5ad0d63
--- /dev/null
+++ b/matchers/not.go
@@ -0,0 +1,23 @@
+package matchers
+
+import "github.com/onsi/gomega/types"
+
+type NotMatcher struct {
+ Matcher types.GomegaMatcher
+}
+
+func (m *NotMatcher) Match(actual interface{}) (bool, error) {
+ success, err := m.Matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ return !success, nil
+}
+
+func (m *NotMatcher) FailureMessage(actual interface{}) (message string) {
+ return m.Matcher.NegatedFailureMessage(actual) // works beautifully
+}
+
+func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ return m.Matcher.FailureMessage(actual) // works beautifully
+}
diff --git a/matchers/not_test.go b/matchers/not_test.go
new file mode 100644
index 0000000..e716178
--- /dev/null
+++ b/matchers/not_test.go
@@ -0,0 +1,43 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("NotMatcher", func() {
+ Context("basic examples", func() {
+ It("works", func() {
+ Expect(input).To(Not(false1))
+ Expect(input).To(Not(Not(true2)))
+ Expect(input).ToNot(Not(true3))
+ Expect(input).ToNot(Not(Not(false1)))
+ Expect(input).To(Not(Not(Not(false2))))
+ })
+ })
+
+ Context("De Morgan's laws", func() {
+ It("~(A && B) == ~A || ~B", func() {
+ Expect(input).To(Not(And(false1, false2)))
+ Expect(input).To(Or(Not(false1), Not(false2)))
+ })
+ It("~(A || B) == ~A && ~B", func() {
+ Expect(input).To(Not(Or(false1, false2)))
+ Expect(input).To(And(Not(false1), Not(false2)))
+ })
+ })
+
+ Context("failure messages are opposite of original matchers' failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(HaveLen(2)), input, "not to have length 2")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(Not(HaveLen(3))), input, "to have length 3")
+ })
+ })
+ })
+})
diff --git a/matchers/or.go b/matchers/or.go
new file mode 100644
index 0000000..228fce6
--- /dev/null
+++ b/matchers/or.go
@@ -0,0 +1,37 @@
+package matchers
+
+import (
+ "fmt"
+ "github.com/onsi/gomega/format"
+ "github.com/onsi/gomega/types"
+)
+
+type OrMatcher struct {
+ Matchers []types.GomegaMatcher
+
+ // state
+ successfulMatcher types.GomegaMatcher
+}
+
+func (m *OrMatcher) Match(actual interface{}) (success bool, err error) {
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ if success {
+ m.successfulMatcher = matcher
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (m *OrMatcher) FailureMessage(actual interface{}) (message string) {
+ // not the most beautiful list of matchers, but not bad either...
+ return format.Message(actual, fmt.Sprintf("To satisfy at least one of these matchers: %s", m.Matchers))
+}
+
+func (m *OrMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ return m.successfulMatcher.NegatedFailureMessage(actual)
+}
diff --git a/matchers/or_test.go b/matchers/or_test.go
new file mode 100644
index 0000000..ae6f975
--- /dev/null
+++ b/matchers/or_test.go
@@ -0,0 +1,43 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("OrMatcher", func() {
+ It("works with positive cases", func() {
+ Expect(input).To(Or(true1))
+ Expect(input).To(Or(true1, true2))
+ Expect(input).To(Or(true1, false1))
+ Expect(input).To(Or(false1, true2))
+ Expect(input).To(Or(true1, true2, true3))
+ Expect(input).To(Or(true1, true2, false3))
+ Expect(input).To(Or(true1, false2, true3))
+ Expect(input).To(Or(false1, true2, true3))
+ Expect(input).To(Or(true1, false2, false3))
+ Expect(input).To(Or(false1, false2, true3))
+ })
+
+ It("works with negative cases", func() {
+ Expect(input).ToNot(Or())
+ Expect(input).ToNot(Or(false1))
+ Expect(input).ToNot(Or(false1, false2))
+ Expect(input).ToNot(Or(false1, false2, false3))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Or(false1, false2), input,
+ "To satisfy at least one of these matchers: [%!s(*matchers.HaveLenMatcher=&{1}) %!s(*matchers.EqualMatcher=&{hip})]")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(Or(true1, true2)), input, `not to have length 2`)
+ })
+ })
+ })
+})
diff --git a/matchers/with_transform.go b/matchers/with_transform.go
new file mode 100644
index 0000000..c7a650a
--- /dev/null
+++ b/matchers/with_transform.go
@@ -0,0 +1,25 @@
+package matchers
+
+import "github.com/onsi/gomega/types"
+
+type WithTransformMatcher struct {
+ // input
+ Transform func(interface{}) interface{}
+ Matcher types.GomegaMatcher
+
+ // state
+ transformedValue interface{}
+}
+
+func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) {
+ m.transformedValue = m.Transform(actual)
+ return m.Matcher.Match(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) FailureMessage(actual interface{}) (message string) {
+ return m.Matcher.FailureMessage(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ return m.Matcher.NegatedFailureMessage(m.transformedValue)
+}
diff --git a/matchers/with_transform_test.go b/matchers/with_transform_test.go
new file mode 100644
index 0000000..ced53bd
--- /dev/null
+++ b/matchers/with_transform_test.go
@@ -0,0 +1,40 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("WithTransformMatcher", func() {
+
+ var plus1 = func(i interface{}) interface{} { return i.(int) + 1 }
+
+ It("works with positive cases", func() {
+ Expect(1).To(WithTransform(plus1, Equal(2)))
+ Expect(1).To(WithTransform(plus1, WithTransform(plus1, Equal(3))))
+ Expect(1).To(WithTransform(plus1, And(Equal(2), BeNumerically(">", 1))))
+ })
+
+ It("works with negative cases", func() {
+ Expect(1).ToNot(WithTransform(plus1, Equal(3)))
+ Expect(1).ToNot(WithTransform(plus1, WithTransform(plus1, Equal(2))))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ m := WithTransform(plus1, Equal(3))
+ Expect(m.Match(1)).To(BeFalse())
+ Expect(m.FailureMessage(input)).To(Equal("Expected\n <int>: 2\nto equal\n <int>: 3"))
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ m := Not(WithTransform(plus1, Equal(3)))
+ Expect(m.Match(2)).To(BeFalse())
+ Expect(m.FailureMessage(input)).To(Equal("Expected\n <int>: 3\nnot to equal\n <int>: 3"))
+ })
+ })
+ })
+})