Skip to content
Merged
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
30 changes: 26 additions & 4 deletions hammy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ Use `Match` when no typed wrapper fits or when the value is intentionally held a

## Dedicated Packages

Use `httpassert` for assertions on `*http.Response` values:
Use `httpassert` for assertions on `*http.Response` values. HTTP assertions use the constructor-style API: wrap the actual value once, then call assertion methods on the returned struct. This is also the preferred model for JSON/YAML constructor-style refactors because the actual value is stored by the wrapper.

```go
import ha "github.com/gogunit/gunit/hammy/httpassert"

assert.Is(ha.Status(resp, http.StatusOK))
assert.Is(ha.Header(resp, "Content-Type", "application/json"))
assert.Is(ha.BodyContains(resp, `"ok":true`))
response := ha.Response(resp)
assert.Is(response.HasStatus(http.StatusOK))
assert.Is(response.HeaderEqualTo("Content-Type", "application/json"))
assert.Is(response.HasBodyContaining(`"ok":true`))
```

Use `jsonassert` for semantic JSON equality that ignores object key order and insignificant whitespace:
Expand Down Expand Up @@ -122,13 +123,34 @@ func Test_payload_has_expected_type(t *testing.T) {

## HTTP (`hammy/httpassert`)

HTTP keeps constructor-style wrappers (`Response(resp)`, `Request(req)`, and `Recorder(rec)`) so actual HTTP values live in assertion structs while methods remain chainable and backward-compatible.

* [x] BodyContains
* [x] BodyEqual
* [x] BodyEqualTo
* [x] BodyMatches
* [x] BodyMatchesRegexp
* [x] Header
* [x] HeaderContains
* [x] HeaderEqualTo
* [x] HasBodyContaining
* [x] HasHeader
* [x] HasHeaderContaining
* [x] HasHost
* [x] HasMethod
* [x] HasPath
* [x] HasQueryParam
* [x] HasStatus
* [x] HasStatusInRange
* [x] Host
* [x] Method
* [x] Path
* [x] QueryParam
* [x] Status
* [x] StatusInRange
* [x] URL
* [x] URLEqual
* [x] URLEqualTo

## JSON (`hammy/jsonassert`)

Expand Down
28 changes: 14 additions & 14 deletions hammy/httpassert/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,52 @@ import (
"github.com/gogunit/gunit/hammy/httpassert"
)

func ExampleResp_Status() {
printExample(httpassert.Response(exampleResponse(http.StatusCreated, nil, "")).Status(http.StatusCreated))
func ExampleResp_HasStatus() {
printExample(httpassert.Response(exampleResponse(http.StatusCreated, nil, "")).HasStatus(http.StatusCreated))
// Output:
// message="got status <201>, wanted <201>"
// success=true
}

func ExampleResp_StatusInRange() {
printExample(httpassert.Response(exampleResponse(http.StatusNoContent, nil, "")).StatusInRange(200, 299))
func ExampleResp_HasStatusInRange() {
printExample(httpassert.Response(exampleResponse(http.StatusNoContent, nil, "")).HasStatusInRange(200, 299))
// Output:
// message="got status <204>, wanted in range <200..299>"
// success=true
}

func ExampleResp_Header() {
func ExampleResp_HeaderEqualTo() {
resp := exampleResponse(http.StatusOK, http.Header{"Content-Type": {"application/json"}}, "")
printExample(httpassert.Response(resp).Header("Content-Type", "application/json"))
printExample(httpassert.Response(resp).HeaderEqualTo("Content-Type", "application/json"))
// Output:
// message="got header <Content-Type>=<application/json>, wanted <application/json>"
// success=true
}

func ExampleResp_HeaderContains() {
func ExampleResp_HasHeaderContaining() {
resp := exampleResponse(http.StatusOK, http.Header{"Content-Type": {"application/json; charset=utf-8"}}, "")
printExample(httpassert.Response(resp).HeaderContains("Content-Type", "application/json"))
printExample(httpassert.Response(resp).HasHeaderContaining("Content-Type", "application/json"))
// Output:
// message="got header <Content-Type>=<application/json; charset=utf-8>, wanted containing <application/json>"
// success=true
}

func ExampleResp_BodyEqual() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "hello world")).BodyEqual("hello world"))
func ExampleResp_BodyEqualTo() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "hello world")).BodyEqualTo("hello world"))
// Output:
// message="got body <hello world>, wanted equal to <hello world>"
// success=true
}

func ExampleResp_BodyContains() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "hello world")).BodyContains("world"))
func ExampleResp_HasBodyContaining() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "hello world")).HasBodyContaining("world"))
// Output:
// message="got body <hello world>, wanted containing <world>"
// success=true
}

func ExampleResp_BodyMatchesRegexp() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "status 204")).BodyMatchesRegexp(`status \d+`))
func ExampleResp_BodyMatches() {
printExample(httpassert.Response(exampleResponse(http.StatusOK, nil, "status 204")).BodyMatches(`status \d+`))
// Output:
// message="got body <status 204>, wanted regexp <status \\d+>"
// success=true
Expand Down
40 changes: 40 additions & 0 deletions hammy/httpassert/httpassert.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ type Resp struct {
actual *http.Response
}

// HasStatus asserts that the response status equals expected. It is an alias for Status.
func (r *Resp) HasStatus(expected int) hammy.AssertionMessage {
return r.Status(expected)
}

func (r *Resp) Status(expected int) hammy.AssertionMessage {
resp := r.response()
if resp == nil {
Expand All @@ -26,6 +31,11 @@ func (r *Resp) Status(expected int) hammy.AssertionMessage {
return hammy.Assert(resp.StatusCode == expected, "got status <%d>, wanted <%d>", resp.StatusCode, expected)
}

// HasStatusInRange asserts that the response status is between min and max, inclusive. It is an alias for StatusInRange.
func (r *Resp) HasStatusInRange(min, max int) hammy.AssertionMessage {
return r.StatusInRange(min, max)
}

func (r *Resp) StatusInRange(min, max int) hammy.AssertionMessage {
resp := r.response()
if min > max {
Expand All @@ -37,6 +47,16 @@ func (r *Resp) StatusInRange(min, max int) hammy.AssertionMessage {
return hammy.Assert(resp.StatusCode >= min && resp.StatusCode <= max, "got status <%d>, wanted in range <%d..%d>", resp.StatusCode, min, max)
}

// HeaderEqualTo asserts that the response header equals expected. It is an alias for Header.
func (r *Resp) HeaderEqualTo(key, expected string) hammy.AssertionMessage {
return r.Header(key, expected)
}

// HasHeader asserts that the response header equals expected. It is an alias for Header.
func (r *Resp) HasHeader(key, expected string) hammy.AssertionMessage {
return r.Header(key, expected)
}

func (r *Resp) Header(key, expected string) hammy.AssertionMessage {
resp := r.response()
if resp == nil {
Expand All @@ -46,6 +66,11 @@ func (r *Resp) Header(key, expected string) hammy.AssertionMessage {
return hammy.Assert(actual == expected, "got header <%s>=<%s>, wanted <%s>", key, actual, expected)
}

// HasHeaderContaining asserts that the response header contains expected. It is an alias for HeaderContains.
func (r *Resp) HasHeaderContaining(key, expected string) hammy.AssertionMessage {
return r.HeaderContains(key, expected)
}

func (r *Resp) HeaderContains(key, expected string) hammy.AssertionMessage {
resp := r.response()
if resp == nil {
Expand All @@ -55,6 +80,11 @@ func (r *Resp) HeaderContains(key, expected string) hammy.AssertionMessage {
return hammy.Assert(strings.Contains(actual, expected), "got header <%s>=<%s>, wanted containing <%s>", key, actual, expected)
}

// BodyEqualTo asserts that the response body equals expected. It is an alias for BodyEqual.
func (r *Resp) BodyEqualTo(expected string) hammy.AssertionMessage {
return r.BodyEqual(expected)
}

func (r *Resp) BodyEqual(expected string) hammy.AssertionMessage {
actual, result := readBody(r.response())
if !result.IsSuccessful {
Expand All @@ -63,6 +93,11 @@ func (r *Resp) BodyEqual(expected string) hammy.AssertionMessage {
return hammy.Assert(actual == expected, "got body <%s>, wanted equal to <%s>", actual, expected)
}

// HasBodyContaining asserts that the response body contains expected. It is an alias for BodyContains.
func (r *Resp) HasBodyContaining(expected string) hammy.AssertionMessage {
return r.BodyContains(expected)
}

func (r *Resp) BodyContains(expected string) hammy.AssertionMessage {
actual, result := readBody(r.response())
if !result.IsSuccessful {
Expand All @@ -71,6 +106,11 @@ func (r *Resp) BodyContains(expected string) hammy.AssertionMessage {
return hammy.Assert(strings.Contains(actual, expected), "got body <%s>, wanted containing <%s>", actual, expected)
}

// BodyMatches asserts that the response body matches pattern. It is an alias for BodyMatchesRegexp.
func (r *Resp) BodyMatches(pattern string) hammy.AssertionMessage {
return r.BodyMatchesRegexp(pattern)
}

func (r *Resp) BodyMatchesRegexp(pattern string) hammy.AssertionMessage {
actual, result := readBody(r.response())
if !result.IsSuccessful {
Expand Down
68 changes: 65 additions & 3 deletions hammy/httpassert/httpassert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,19 @@ func Test_BodyMatchesRegexp_failure_invalid_pattern(t *testing.T) {
aSpy.HadErrorContaining(t, "invalid regexp <(>")
}

func Test_Body_assertions_restore_body(t *testing.T) {
func Test_Body_assertion_restores_body(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "hello world")

assert.Is(httpassert.Response(resp).BodyContains("hello"))
assert.Is(httpassert.Response(resp).BodyEqual("hello world"))
assert.Is(httpassert.Response(resp).BodyMatchesRegexp(`world$`))

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read restored body: %v", err)
}
if string(body) != "hello world" {
t.Fatalf("got restored body %q, wanted %q", string(body), "hello world")
}
}

func Test_Body_assertion_failure_nil_response(t *testing.T) {
Expand Down Expand Up @@ -218,3 +224,59 @@ func Test_Response_methods_failure_nil_receiver(t *testing.T) {
assert.Is(result)
aSpy.HadErrorContaining(t, "got nil response")
}

func Test_Response_HasStatus_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "")

assert.Is(httpassert.Response(resp).HasStatus(http.StatusOK))
}

func Test_Response_HasStatusInRange_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "")

assert.Is(httpassert.Response(resp).HasStatusInRange(200, 299))
}

func Test_Response_HeaderEqualTo_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, http.Header{"Content-Type": {"application/json; charset=utf-8"}}, "")

assert.Is(httpassert.Response(resp).HeaderEqualTo("Content-Type", "application/json; charset=utf-8"))
}

func Test_Response_HasHeader_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, http.Header{"Content-Type": {"application/json; charset=utf-8"}}, "")

assert.Is(httpassert.Response(resp).HasHeader("Content-Type", "application/json; charset=utf-8"))
}

func Test_Response_HasHeaderContaining_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, http.Header{"Content-Type": {"application/json; charset=utf-8"}}, "")

assert.Is(httpassert.Response(resp).HasHeaderContaining("Content-Type", "application/json"))
}

func Test_Response_BodyEqualTo_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "status 204")

assert.Is(httpassert.Response(resp).BodyEqualTo("status 204"))
}

func Test_Response_HasBodyContaining_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "status 204")

assert.Is(httpassert.Response(resp).HasBodyContaining("204"))
}

func Test_Response_BodyMatches_alias_success(t *testing.T) {
assert := hammy.New(t)
resp := newResponse(http.StatusOK, nil, "status 204")

assert.Is(httpassert.Response(resp).BodyMatches(`status \d+`))
}
38 changes: 38 additions & 0 deletions hammy/httpassert/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type RecorderAssert struct {
actual *httptest.ResponseRecorder
}

// HasStatus asserts that the recorded response status equals expected. It is an alias for Status.
func (r *RecorderAssert) HasStatus(expected int) hammy.AssertionMessage { return r.Status(expected) }

// Status asserts that the recorded response status equals expected.
func (r *RecorderAssert) Status(expected int) hammy.AssertionMessage {
resp, result := r.response()
Expand All @@ -28,6 +31,11 @@ func (r *RecorderAssert) Status(expected int) hammy.AssertionMessage {
return hammy.Assert(resp.StatusCode == expected, "got status <%d>, wanted <%d>", resp.StatusCode, expected)
}

// HasStatusInRange asserts that the recorded response status is between min and max, inclusive. It is an alias for StatusInRange.
func (r *RecorderAssert) HasStatusInRange(min, max int) hammy.AssertionMessage {
return r.StatusInRange(min, max)
}

// StatusInRange asserts that the recorded response status is between min and max, inclusive.
func (r *RecorderAssert) StatusInRange(min, max int) hammy.AssertionMessage {
if min > max {
Expand All @@ -40,6 +48,16 @@ func (r *RecorderAssert) StatusInRange(min, max int) hammy.AssertionMessage {
return hammy.Assert(resp.StatusCode >= min && resp.StatusCode <= max, "got status <%d>, wanted in range <%d..%d>", resp.StatusCode, min, max)
}

// HeaderEqualTo asserts that the recorded response header equals expected. It is an alias for Header.
func (r *RecorderAssert) HeaderEqualTo(key, expected string) hammy.AssertionMessage {
return r.Header(key, expected)
}

// HasHeader asserts that the recorded response header equals expected. It is an alias for Header.
func (r *RecorderAssert) HasHeader(key, expected string) hammy.AssertionMessage {
return r.Header(key, expected)
}

// Header asserts that the recorded response header equals expected.
func (r *RecorderAssert) Header(key, expected string) hammy.AssertionMessage {
resp, result := r.response()
Expand All @@ -50,6 +68,11 @@ func (r *RecorderAssert) Header(key, expected string) hammy.AssertionMessage {
return hammy.Assert(actual == expected, "got header <%s>=<%s>, wanted <%s>", key, actual, expected)
}

// HasHeaderContaining asserts that the recorded response header contains expected. It is an alias for HeaderContains.
func (r *RecorderAssert) HasHeaderContaining(key, expected string) hammy.AssertionMessage {
return r.HeaderContains(key, expected)
}

// HeaderContains asserts that the recorded response header contains expected.
func (r *RecorderAssert) HeaderContains(key, expected string) hammy.AssertionMessage {
resp, result := r.response()
Expand All @@ -60,6 +83,11 @@ func (r *RecorderAssert) HeaderContains(key, expected string) hammy.AssertionMes
return hammy.Assert(strings.Contains(actual, expected), "got header <%s>=<%s>, wanted containing <%s>", key, actual, expected)
}

// BodyEqualTo asserts that the recorded response body equals expected. It is an alias for BodyEqual.
func (r *RecorderAssert) BodyEqualTo(expected string) hammy.AssertionMessage {
return r.BodyEqual(expected)
}

// BodyEqual asserts that the recorded response body equals expected.
func (r *RecorderAssert) BodyEqual(expected string) hammy.AssertionMessage {
actual, result := r.body()
Expand All @@ -69,6 +97,11 @@ func (r *RecorderAssert) BodyEqual(expected string) hammy.AssertionMessage {
return hammy.Assert(actual == expected, "got body <%s>, wanted equal to <%s>", actual, expected)
}

// HasBodyContaining asserts that the recorded response body contains expected. It is an alias for BodyContains.
func (r *RecorderAssert) HasBodyContaining(expected string) hammy.AssertionMessage {
return r.BodyContains(expected)
}

// BodyContains asserts that the recorded response body contains expected.
func (r *RecorderAssert) BodyContains(expected string) hammy.AssertionMessage {
actual, result := r.body()
Expand All @@ -78,6 +111,11 @@ func (r *RecorderAssert) BodyContains(expected string) hammy.AssertionMessage {
return hammy.Assert(strings.Contains(actual, expected), "got body <%s>, wanted containing <%s>", actual, expected)
}

// BodyMatches asserts that the recorded response body matches pattern. It is an alias for BodyMatchesRegexp.
func (r *RecorderAssert) BodyMatches(pattern string) hammy.AssertionMessage {
return r.BodyMatchesRegexp(pattern)
}

// BodyMatchesRegexp asserts that the recorded response body matches pattern.
func (r *RecorderAssert) BodyMatchesRegexp(pattern string) hammy.AssertionMessage {
actual, result := r.body()
Expand Down
Loading
Loading