Skip to content

Commit

Permalink
BeTrueBecause and BeFalseBecause allow for better failure messages
Browse files Browse the repository at this point in the history
  • Loading branch information
onsi committed Nov 8, 2023
1 parent 6ca6e97 commit 4da4c7f
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 47 deletions.
30 changes: 28 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
78 changes: 39 additions & 39 deletions ghttp/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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})
}
Expand All @@ -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) {
Expand All @@ -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"),
Expand All @@ -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())
Expand All @@ -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()
Expand All @@ -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"),
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions matchers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gomega

import (
"fmt"
"time"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -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:
//
Expand Down
13 changes: 11 additions & 2 deletions matchers/be_false_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

type BeFalseMatcher struct {
Reason string
}

func (matcher *BeFalseMatcher) Match(actual interface{}) (success bool, err error) {
Expand All @@ -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)
}
}
26 changes: 25 additions & 1 deletion matchers/be_false_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 <bool>: 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 <bool>: 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`))
})
})
13 changes: 11 additions & 2 deletions matchers/be_true_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

type BeTrueMatcher struct {
Reason string
}

func (matcher *BeTrueMatcher) Match(actual interface{}) (success bool, err error) {
Expand All @@ -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)
}
}
Loading

0 comments on commit 4da4c7f

Please sign in to comment.