From 4da4c7fa5b9a2065b077570854314406fe495464 Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Wed, 8 Nov 2023 09:27:14 -0700 Subject: [PATCH] BeTrueBecause and BeFalseBecause allow for better failure messages --- docs/index.md | 30 +++++++++++- ghttp/handlers.go | 78 +++++++++++++++---------------- matchers.go | 17 +++++++ matchers/be_false_matcher.go | 13 +++++- matchers/be_false_matcher_test.go | 26 ++++++++++- matchers/be_true_matcher.go | 13 +++++- matchers/be_true_matcher_test.go | 26 ++++++++++- 7 files changed, 156 insertions(+), 47 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5fefcae87..acdcbcbe8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -768,17 +768,43 @@ succeeds if `ACTUAL` is the zero value for its type *or* if `ACTUAL` is `nil`. Ω(ACTUAL).Should(BeTrue()) ``` -succeeds if `ACTUAL` is `bool` typed and has the value `true`. It is an error for `ACTUAL` to not be a `bool`. +succeeds if `ACTUAL` is `bool` typed and has the value `true`. It is an error for `ACTUAL` to not be a `bool`. + +Since Gomega has no additional context about your assertion the failure messages are generally not particularly helpful. So it's generally recommended that you use `BeTrueBecause` instead. > Some matcher libraries have a notion of "truthiness" to assert that an object is present. Gomega is strict, and `BeTrue()` only works with `bool`s. You can use `Ω(ACTUAL).ShouldNot(BeZero())` or `Ω(ACTUAL).ShouldNot(BeNil())` to verify object presence. +### BeTrueBecause(reason) + +```go +Ω(ACTUAL).Should(BeTrueBecause(REASON, ARGS...)) +``` + +is just like `BeTrue()` but allows you to pass in a reason. This is a best practice as the default failure message is not particularly helpful. `fmt.Sprintf(REASON, ARGS...)` is used to render the reason. For example: + +```go +Ω(cow.JumpedOver(moon)).Should(BeTrueBecause("the cow should have jumped over the moon")) +``` + #### BeFalse() ```go Ω(ACTUAL).Should(BeFalse()) ``` -succeeds if `ACTUAL` is `bool` typed and has the value `false`. It is an error for `ACTUAL` to not be a `bool`. +succeeds if `ACTUAL` is `bool` typed and has the value `false`. It is an error for `ACTUAL` to not be a `bool`. You should generaly use `BeFalseBecause` instead to pas in a reason for a more helpful error message. + +### BeFalseBecause(reason) + +```go +Ω(ACTUAL).Should(BeFalseBecause(REASON, ARGS...)) +``` + +is just like `BeFalse()` but allows you to pass in a reason. This is a best practice as the default failure message is not particularly helpful. `fmt.Sprintf(REASON, ARGS...)` is used to render the reason. + +```go +Ω(cow.JumpedOver(mars)).Should(BeFalseBecause("the cow should not have jumped over mars")) +``` ### Asserting on Errors diff --git a/ghttp/handlers.go b/ghttp/handlers.go index fa7fc0ba3..b2d1c2c46 100644 --- a/ghttp/handlers.go +++ b/ghttp/handlers.go @@ -28,8 +28,8 @@ func NewGHTTPWithGomega(gomega Gomega) *GHTTPWithGomega { } } -//CombineHandler takes variadic list of handlers and produces one handler -//that calls each handler in order. +// CombineHandler takes variadic list of handlers and produces one handler +// that calls each handler in order. func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { for _, handler := range handlers { @@ -38,11 +38,11 @@ func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFunc { } } -//VerifyRequest returns a handler that verifies that a request uses the specified method to connect to the specified path -//You may also pass in an optional rawQuery string which is tested against the request's `req.URL.RawQuery` +// VerifyRequest returns a handler that verifies that a request uses the specified method to connect to the specified path +// You may also pass in an optional rawQuery string which is tested against the request's `req.URL.RawQuery` // -//For path, you may pass in a string, in which case strict equality will be applied -//Alternatively you can pass in a matcher (ContainSubstring("/foo") and MatchRegexp("/foo/[a-f0-9]+") for example) +// For path, you may pass in a string, in which case strict equality will be applied +// Alternatively you can pass in a matcher (ContainSubstring("/foo") and MatchRegexp("/foo/[a-f0-9]+") for example) func (g GHTTPWithGomega) VerifyRequest(method string, path interface{}, rawQuery ...string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { g.gomega.Expect(req.Method).Should(Equal(method), "Method mismatch") @@ -61,24 +61,24 @@ func (g GHTTPWithGomega) VerifyRequest(method string, path interface{}, rawQuery } } -//VerifyContentType returns a handler that verifies that a request has a Content-Type header set to the -//specified value +// VerifyContentType returns a handler that verifies that a request has a Content-Type header set to the +// specified value func (g GHTTPWithGomega) VerifyContentType(contentType string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { g.gomega.Expect(req.Header.Get("Content-Type")).Should(Equal(contentType)) } } -//VerifyMimeType returns a handler that verifies that a request has a specified mime type set -//in Content-Type header +// VerifyMimeType returns a handler that verifies that a request has a specified mime type set +// in Content-Type header func (g GHTTPWithGomega) VerifyMimeType(mimeType string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { g.gomega.Expect(strings.Split(req.Header.Get("Content-Type"), ";")[0]).Should(Equal(mimeType)) } } -//VerifyBasicAuth returns a handler that verifies the request contains a BasicAuth Authorization header -//matching the passed in username and password +// VerifyBasicAuth returns a handler that verifies the request contains a BasicAuth Authorization header +// matching the passed in username and password func (g GHTTPWithGomega) VerifyBasicAuth(username string, password string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { auth := req.Header.Get("Authorization") @@ -91,11 +91,11 @@ func (g GHTTPWithGomega) VerifyBasicAuth(username string, password string) http. } } -//VerifyHeader returns a handler that verifies the request contains the passed in headers. -//The passed in header keys are first canonicalized via http.CanonicalHeaderKey. +// VerifyHeader returns a handler that verifies the request contains the passed in headers. +// The passed in header keys are first canonicalized via http.CanonicalHeaderKey. // -//The request must contain *all* the passed in headers, but it is allowed to have additional headers -//beyond the passed in set. +// The request must contain *all* the passed in headers, but it is allowed to have additional headers +// beyond the passed in set. func (g GHTTPWithGomega) VerifyHeader(header http.Header) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { for key, values := range header { @@ -105,9 +105,9 @@ func (g GHTTPWithGomega) VerifyHeader(header http.Header) http.HandlerFunc { } } -//VerifyHeaderKV returns a handler that verifies the request contains a header matching the passed in key and values -//(recall that a `http.Header` is a mapping from string (key) to []string (values)) -//It is a convenience wrapper around `VerifyHeader` that allows you to avoid having to create an `http.Header` object. +// VerifyHeaderKV returns a handler that verifies the request contains a header matching the passed in key and values +// (recall that a `http.Header` is a mapping from string (key) to []string (values)) +// It is a convenience wrapper around `VerifyHeader` that allows you to avoid having to create an `http.Header` object. func (g GHTTPWithGomega) VerifyHeaderKV(key string, values ...string) http.HandlerFunc { return g.VerifyHeader(http.Header{key: values}) } @@ -127,8 +127,8 @@ func (g GHTTPWithGomega) VerifyHost(host interface{}) http.HandlerFunc { } } -//VerifyBody returns a handler that verifies that the body of the request matches the passed in byte array. -//It does this using Equal(). +// VerifyBody returns a handler that verifies that the body of the request matches the passed in byte array. +// It does this using Equal(). func (g GHTTPWithGomega) VerifyBody(expectedBody []byte) http.HandlerFunc { return CombineHandlers( func(w http.ResponseWriter, req *http.Request) { @@ -140,10 +140,10 @@ func (g GHTTPWithGomega) VerifyBody(expectedBody []byte) http.HandlerFunc { ) } -//VerifyJSON returns a handler that verifies that the body of the request is a valid JSON representation -//matching the passed in JSON string. It does this using Gomega's MatchJSON method +// VerifyJSON returns a handler that verifies that the body of the request is a valid JSON representation +// matching the passed in JSON string. It does this using Gomega's MatchJSON method // -//VerifyJSON also verifies that the request's content type is application/json +// VerifyJSON also verifies that the request's content type is application/json func (g GHTTPWithGomega) VerifyJSON(expectedJSON string) http.HandlerFunc { return CombineHandlers( g.VerifyMimeType("application/json"), @@ -156,9 +156,9 @@ func (g GHTTPWithGomega) VerifyJSON(expectedJSON string) http.HandlerFunc { ) } -//VerifyJSONRepresenting is similar to VerifyJSON. Instead of taking a JSON string, however, it -//takes an arbitrary JSON-encodable object and verifies that the requests's body is a JSON representation -//that matches the object +// VerifyJSONRepresenting is similar to VerifyJSON. Instead of taking a JSON string, however, it +// takes an arbitrary JSON-encodable object and verifies that the requests's body is a JSON representation +// that matches the object func (g GHTTPWithGomega) VerifyJSONRepresenting(object interface{}) http.HandlerFunc { data, err := json.Marshal(object) g.gomega.Expect(err).ShouldNot(HaveOccurred()) @@ -168,10 +168,10 @@ func (g GHTTPWithGomega) VerifyJSONRepresenting(object interface{}) http.Handler ) } -//VerifyForm returns a handler that verifies a request contains the specified form values. +// VerifyForm returns a handler that verifies a request contains the specified form values. // -//The request must contain *all* of the specified values, but it is allowed to have additional -//form values beyond the passed in set. +// The request must contain *all* of the specified values, but it is allowed to have additional +// form values beyond the passed in set. func (g GHTTPWithGomega) VerifyForm(values url.Values) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() @@ -182,17 +182,17 @@ func (g GHTTPWithGomega) VerifyForm(values url.Values) http.HandlerFunc { } } -//VerifyFormKV returns a handler that verifies a request contains a form key with the specified values. +// VerifyFormKV returns a handler that verifies a request contains a form key with the specified values. // -//It is a convenience wrapper around `VerifyForm` that lets you avoid having to create a `url.Values` object. +// It is a convenience wrapper around `VerifyForm` that lets you avoid having to create a `url.Values` object. func (g GHTTPWithGomega) VerifyFormKV(key string, values ...string) http.HandlerFunc { return g.VerifyForm(url.Values{key: values}) } -//VerifyProtoRepresenting returns a handler that verifies that the body of the request is a valid protobuf -//representation of the passed message. +// VerifyProtoRepresenting returns a handler that verifies that the body of the request is a valid protobuf +// representation of the passed message. // -//VerifyProtoRepresenting also verifies that the request's content type is application/x-protobuf +// VerifyProtoRepresenting also verifies that the request's content type is application/x-protobuf func (g GHTTPWithGomega) VerifyProtoRepresenting(expected proto.Message) http.HandlerFunc { return CombineHandlers( g.VerifyContentType("application/x-protobuf"), @@ -205,7 +205,7 @@ func (g GHTTPWithGomega) VerifyProtoRepresenting(expected proto.Message) http.Ha actualValuePtr := reflect.New(expectedType.Elem()) actual, ok := actualValuePtr.Interface().(proto.Message) - g.gomega.Expect(ok).Should(BeTrue(), "Message value is not a proto.Message") + g.gomega.Expect(ok).Should(BeTrueBecause("Message value should be a proto.Message")) err = proto.Unmarshal(body, actual) g.gomega.Expect(err).ShouldNot(HaveOccurred(), "Failed to unmarshal protobuf") @@ -324,10 +324,10 @@ func (g GHTTPWithGomega) RespondWithJSONEncodedPtr(statusCode *int, object inter } } -//RespondWithProto returns a handler that responds to a request with the specified status code and a body -//containing the protobuf serialization of the provided message. +// RespondWithProto returns a handler that responds to a request with the specified status code and a body +// containing the protobuf serialization of the provided message. // -//Also, RespondWithProto can be given an optional http.Header. The headers defined therein will be added to the response headers. +// Also, RespondWithProto can be given an optional http.Header. The headers defined therein will be added to the response headers. func (g GHTTPWithGomega) RespondWithProto(statusCode int, message proto.Message, optionalHeader ...http.Header) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { data, err := proto.Marshal(message) diff --git a/matchers.go b/matchers.go index cd3f431d2..43f994374 100644 --- a/matchers.go +++ b/matchers.go @@ -1,6 +1,7 @@ package gomega import ( + "fmt" "time" "github.com/google/go-cmp/cmp" @@ -52,15 +53,31 @@ func BeNil() types.GomegaMatcher { } // BeTrue succeeds if actual is true +// +// In general, it's better to use `BeTrueBecause(reason)` to provide a more useful error message if a true check fails. func BeTrue() types.GomegaMatcher { return &matchers.BeTrueMatcher{} } // BeFalse succeeds if actual is false +// +// In general, it's better to use `BeFalseBecause(reason)` to provide a more useful error message if a false check fails. func BeFalse() types.GomegaMatcher { return &matchers.BeFalseMatcher{} } +// BeTrueBecause succeeds if actual is true and displays the provided reason if it is false +// fmt.Sprintf is used to render the reason +func BeTrueBecause(format string, args ...any) types.GomegaMatcher { + return &matchers.BeTrueMatcher{Reason: fmt.Sprintf(format, args...)} +} + +// BeFalseBecause succeeds if actual is false and displays the provided reason if it is true. +// fmt.Sprintf is used to render the reason +func BeFalseBecause(format string, args ...any) types.GomegaMatcher { + return &matchers.BeFalseMatcher{Reason: fmt.Sprintf(format, args...)} +} + // HaveOccurred succeeds if actual is a non-nil error // The typical Go error checking pattern looks like: // diff --git a/matchers/be_false_matcher.go b/matchers/be_false_matcher.go index e326c0157..8ee2b1c51 100644 --- a/matchers/be_false_matcher.go +++ b/matchers/be_false_matcher.go @@ -9,6 +9,7 @@ import ( ) type BeFalseMatcher struct { + Reason string } func (matcher *BeFalseMatcher) Match(actual interface{}) (success bool, err error) { @@ -20,9 +21,17 @@ func (matcher *BeFalseMatcher) Match(actual interface{}) (success bool, err erro } func (matcher *BeFalseMatcher) FailureMessage(actual interface{}) (message string) { - return format.Message(actual, "to be false") + if matcher.Reason == "" { + return format.Message(actual, "to be false") + } else { + return matcher.Reason + } } func (matcher *BeFalseMatcher) NegatedFailureMessage(actual interface{}) (message string) { - return format.Message(actual, "not to be false") + if matcher.Reason == "" { + return format.Message(actual, "not to be false") + } else { + return fmt.Sprintf(`Expected not false but got false\nNegation of "%s" failed`, matcher.Reason) + } } diff --git a/matchers/be_false_matcher_test.go b/matchers/be_false_matcher_test.go index 2ee4b3120..f60b28af7 100644 --- a/matchers/be_false_matcher_test.go +++ b/matchers/be_false_matcher_test.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/gomega/matchers" ) -var _ = Describe("BeFalse", func() { +var _ = Describe("BeFalse and BeFalseBecause", func() { It("should handle true and false correctly", func() { Expect(true).ShouldNot(BeFalse()) Expect(false).Should(BeFalse()) @@ -17,4 +17,28 @@ var _ = Describe("BeFalse", func() { Expect(success).Should(BeFalse()) Expect(err).Should(HaveOccurred()) }) + + It("returns the passed in failure message if provided", func() { + x := 100 + err := InterceptGomegaFailure(func() { Expect(x == 100).Should(BeFalse()) }) + Ω(err.Error()).Should(Equal("Expected\n : true\nto be false")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).Should(BeFalseBecause("x should not be 100%%")) }) + Ω(err.Error()).Should(Equal("x should not be 100%")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).Should(BeFalseBecause("x should not be %d%%", 100)) }) + Ω(err.Error()).Should(Equal("x should not be 100%")) + }) + + It("prints out a useful message if a negation fails", func() { + x := 10 + err := InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeFalse()) }) + Ω(err.Error()).Should(Equal("Expected\n : false\nnot to be false")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeFalseBecause("x should not be 100%%")) }) + Ω(err.Error()).Should(Equal(`Expected not false but got false\nNegation of "x should not be 100%" failed`)) + + err = InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeFalseBecause("x should not be %d%%", 100)) }) + Ω(err.Error()).Should(Equal(`Expected not false but got false\nNegation of "x should not be 100%" failed`)) + }) }) diff --git a/matchers/be_true_matcher.go b/matchers/be_true_matcher.go index 60bc1e3fa..3576aac88 100644 --- a/matchers/be_true_matcher.go +++ b/matchers/be_true_matcher.go @@ -9,6 +9,7 @@ import ( ) type BeTrueMatcher struct { + Reason string } func (matcher *BeTrueMatcher) Match(actual interface{}) (success bool, err error) { @@ -20,9 +21,17 @@ func (matcher *BeTrueMatcher) Match(actual interface{}) (success bool, err error } func (matcher *BeTrueMatcher) FailureMessage(actual interface{}) (message string) { - return format.Message(actual, "to be true") + if matcher.Reason == "" { + return format.Message(actual, "to be true") + } else { + return matcher.Reason + } } func (matcher *BeTrueMatcher) NegatedFailureMessage(actual interface{}) (message string) { - return format.Message(actual, "not to be true") + if matcher.Reason == "" { + return format.Message(actual, "not to be true") + } else { + return fmt.Sprintf(`Expected not true but got true\nNegation of "%s" failed`, matcher.Reason) + } } diff --git a/matchers/be_true_matcher_test.go b/matchers/be_true_matcher_test.go index 3c4f9198e..f2daea3df 100644 --- a/matchers/be_true_matcher_test.go +++ b/matchers/be_true_matcher_test.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/gomega/matchers" ) -var _ = Describe("BeTrue", func() { +var _ = Describe("BeTrue and BeTrueBecause", func() { It("should handle true and false correctly", func() { Expect(true).Should(BeTrue()) Expect(false).ShouldNot(BeTrue()) @@ -17,4 +17,28 @@ var _ = Describe("BeTrue", func() { Expect(success).Should(BeFalse()) Expect(err).Should(HaveOccurred()) }) + + It("returns the passed in failure message if provided", func() { + x := 10 + err := InterceptGomegaFailure(func() { Expect(x == 100).Should(BeTrue()) }) + Ω(err.Error()).Should(Equal("Expected\n : false\nto be true")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).Should(BeTrueBecause("x should be 100%%")) }) + Ω(err.Error()).Should(Equal("x should be 100%")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).Should(BeTrueBecause("x should be %d%%", 100)) }) + Ω(err.Error()).Should(Equal("x should be 100%")) + }) + + It("prints out a useful message if a negation fails", func() { + x := 100 + err := InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeTrue()) }) + Ω(err.Error()).Should(Equal("Expected\n : true\nnot to be true")) + + err = InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeTrueBecause("x should be 100%%")) }) + Ω(err.Error()).Should(Equal(`Expected not true but got true\nNegation of "x should be 100%" failed`)) + + err = InterceptGomegaFailure(func() { Expect(x == 100).ShouldNot(BeTrueBecause("x should be %d%%", 100)) }) + Ω(err.Error()).Should(Equal(`Expected not true but got true\nNegation of "x should be 100%" failed`)) + }) })