From 7302f7c4cf810981728a9fa569e59bcee9fbd406 Mon Sep 17 00:00:00 2001 From: George Psarakis Date: Sun, 6 Oct 2024 08:17:26 +0300 Subject: [PATCH] Add documentation --- README.md | 324 +++++++++++++++++- client.go | 6 + examples/example_client_json_api_test.go | 35 ++ .../example_client_json_api_testing_test.go | 62 ++++ .../example_net_http_client_json_api_test.go | 129 +++++++ examples/githubsdk/githubsdk.go | 54 +++ httpassert/response.go | 7 +- httpassert/response_test.go | 44 +++ httptesting/client.go | 5 + httptesting/client_test.go | 9 +- 10 files changed, 669 insertions(+), 6 deletions(-) create mode 100644 examples/example_client_json_api_test.go create mode 100644 examples/example_client_json_api_testing_test.go create mode 100644 examples/example_net_http_client_json_api_test.go create mode 100644 examples/githubsdk/githubsdk.go diff --git a/README.md b/README.md index 0795362..5d2b845 100644 --- a/README.md +++ b/README.md @@ -1 +1,323 @@ -# go-httpclient \ No newline at end of file +# go-httpclient + +## Summary + +**go-httpclient** aims to reduce the boilerplate of HTTP request/response +setup for Go, along with providing out-of-the-box testing in a standardized way. + +The Go standard library [net/http](https://pkg.go.dev/net/http) has already an excellent & powerful API. +However, the complexity of reliably & securely composing an HTTP request or reading back the HTTP response +cannot be easily avoided without a higher-level abstraction layer. + +**go-httpclient** also tries to enforce best practices such as: +- Non-zero request timeout +- Passing `context.Context` to `net/http` +- URL-encoded Query Parameters +- Safe URL Path Joining +- Always closing the response body + +Furthermore, testing is facilitated by the `httptesting` & `httpassert` libraries. +`httptesting` provides a 100% compatible API with `httpclient.Client` and exposes a `httpclient.Client` instance that +can be injected directly as a drop-in replacement of the regular production code `Client`. +The testing abstraction layer is using a [httpmock](https://github.com/jarcoal/httpmock) Mock Transport under the hood +which allows registration of custom matcher/responders when required. + +## Key Features + +- Offers an intuitive and ergonomic API based on HTTP verb names `Get, Post, Patch, Delete` and functional option parameters. +- All request emitter methods accept `context.Context` as their first parameter. +- Uses plain `map[string]string` structures for passing Query Parameters and Headers which should cover the majority of cases. +- Always URL-encodes query parameters. +- Ensures Response body is read when streaming is not required. +- Separate testing `Client` that implements the exact same API. +- Utilizes the powerful [httpmock](https://github.com/jarcoal/httpmock) under the hood in order to allow fine-grained and + scoped request mocking and assertions. + +## Examples + +Example implementation of a GitHub REST API SDK using `httpclient.Client`: + +```go +package githubsdk + +import ( + "context" + "net/url" + "time" + + "github.com/georgepsarakis/go-httpclient" +) + +type GitHubSDK struct { + Client *httpclient.Client +} + +func New() GitHubSDK { + client := httpclient.New() + return NewWithClient(client) +} + +func NewWithClient(c *httpclient.Client) GitHubSDK { + c.WithDefaultHeaders(map[string]string{ + "X-GitHub-Api-Version": "2022-11-28", + "Accept": "application/vnd.github+json", + }) + c, _ = c.WithBaseURL("https://api.github.com") + return GitHubSDK{Client: c} +} + +type User struct { + ID int `json:"id"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt time.Time `json:"created_at"` + Login string `json:"login"` + Name string `json:"name"` +} + +// GetUserByUsername retrieves a user based on their public username. +// See https://docs.github.com/en/rest/users/users +func (g GitHubSDK) GetUserByUsername(ctx context.Context, username string) (User, error) { + path, err := url.JoinPath("/users", username) + if err != nil { + return User{}, err + } + // Note: `httpclient.Client.Get` allows header parameterization, for example changing an API version: + // resp, err := g.Client.Get(ctx, path, httpclient.WithHeaders(map[string]string{"x-github-api-version": "2023-11-22"})) + resp, err := g.Client.Get(ctx, path) + if err != nil { + return User{}, err + } + u := User{} + if err := httpclient.DeserializeJSON(resp, &u); err != nil { + return u, err + } + return u, nil +} +``` + +Here is how using our SDK looks like: + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/georgepsarakis/go-httpclient/examples/githubsdk" +) + +func main() { + sdk := githubsdk.New() + + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + panicOnError(err) + + m, err := json.MarshalIndent(user, "", " ") + panicOnError(err) + + fmt.Println(string(m)) + // Output: + //{ + // "id": 963304, + // "bio": "Software Engineer", + // "blog": "https://controlflow.substack.com/", + // "created_at": "2011-08-06T16:57:12Z", + // "login": "georgepsarakis", + // "name": "George Psarakis" + //} +} + +func panicOnError(err error) { + if err != nil { + panic(err) + } +} +``` + +Testing our GitHub SDK as well as code that depends on it is straightforward thanks to the `httptesting` package: + +```go +package githubsdk_test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/georgepsarakis/go-httpclient" + "github.com/georgepsarakis/go-httpclient/examples/githubsdk" + "github.com/georgepsarakis/go-httpclient/httpassert" + "github.com/georgepsarakis/go-httpclient/httptesting" +) + +func TestGitHubSDK_GetUserByUsername(t *testing.T) { + var err error + + testClient := httptesting.NewClient(t) + sdk := githubsdk.NewWithClient(testClient.Client) + testClient, err = testClient.WithBaseURL("https://test-api-github-com") + require.NoError(t, err) + + testClient. + NewMockRequest( + http.MethodGet, + "https://test-api-github-com/users/georgepsarakis", + httpclient.WithHeaders(map[string]string{ + "x-github-api-version": "2022-11-28", + "accept": "application/vnd.github+json", + })). + RespondWithJSON(http.StatusOK, ` + { + "id": 963304, + "bio": "Test 123", + "blog": "https://test.blog/", + "created_at": "2025-09-16T16:57:12Z", + "login": "georgepsarakis", + "name": "Test Name" + }`).Register() + + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + require.NoError(t, err) + + httpassert.PrintJSON(t, user) + require.Equal(t, githubsdk.User{ + ID: 963304, + Bio: "Test 123", + Blog: "https://test.blog/", + CreatedAt: time.Date(2025, time.September, 16, 16, 57, 12, 0, time.UTC), + Login: "georgepsarakis", + Name: "Test Name", + }, user) +} +``` + +For comparison, below is an alternative implementation using the `net/http` & `httpmock` packages: + +```go +package examples_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + "time" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" + + "github.com/georgepsarakis/go-httpclient/httpassert" +) + +type GitHubSDK struct { + Client *http.Client + DefaultHeaders http.Header + BaseURL string +} + +func New() GitHubSDK { + client := http.DefaultClient + return NewWithClient(client) +} + +func NewWithClient(c *http.Client) GitHubSDK { + headers := http.Header{} + headers.Set("X-GitHub-Api-Version", "2022-11-28") + headers.Set("Accept", "application/vnd.github+json") + return GitHubSDK{Client: c, DefaultHeaders: headers, BaseURL: "https://api.github.com"} +} + +type User struct { + ID int `json:"id"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt time.Time `json:"created_at"` + Login string `json:"login"` + Name string `json:"name"` +} + +// GetUserByUsername retrieves a user based on their public username. +// See https://docs.github.com/en/rest/users/users +func (g GitHubSDK) GetUserByUsername(ctx context.Context, username string) (User, error) { + path, err := url.JoinPath("/users", username) + if err != nil { + return User{}, err + } + fullURL := fmt.Sprintf("%s%s", g.BaseURL, path) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) + if err != nil { + return User{}, err + } + req.Header = g.DefaultHeaders.Clone() + resp, err := g.Client.Do(req) + if err != nil { + return User{}, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return User{}, err + } + u := User{} + if err := json.Unmarshal(b, &u); err != nil { + return User{}, err + } + return u, nil +} + +func TestGitHubSDK_NetHTTP_GetUserByUsername(t *testing.T) { + mt := httpmock.NewMockTransport() + testClient := &http.Client{ + Transport: mt, + } + sdk := NewWithClient(testClient) + sdk.BaseURL = "https://test-api-github-com" + + responder, err := httpmock.NewJsonResponder(http.StatusOK, json.RawMessage(` + { + "id": 963304, + "bio": "Test 123", + "blog": "https://test.blog/", + "created_at": "2025-09-16T16:57:12Z", + "login": "georgepsarakis", + "name": "Test Name" + }`)) + require.NoError(t, err) + + defaultHeaderMatcher := func(req *http.Request) bool { + return req.Header.Get("Accept") == "application/vnd.github+json" && + req.Header.Get("X-GitHub-Api-Version") == "2022-11-28" + } + mt.RegisterMatcherResponder(http.MethodGet, + "https://test-api-github-com/users/georgepsarakis", + httpmock.NewMatcher("GetUserByUsername", func(req *http.Request) bool { + if !defaultHeaderMatcher(req) { + return false + } + return true + }), + responder) + + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + require.NoError(t, err) + + httpassert.PrintJSON(t, user) + require.Equal(t, User{ + ID: 963304, + Bio: "Test 123", + Blog: "https://test.blog/", + CreatedAt: time.Date(2025, time.September, 16, 16, 57, 12, 0, time.UTC), + Login: "georgepsarakis", + Name: "Test Name", + }, user) +} +``` diff --git a/client.go b/client.go index 3874a5a..5d49300 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,8 @@ func New() *Client { return NewWithTransport(http.DefaultTransport) } +// NewWithTransport creates a new Client object that uses the given http.Roundtripper +// as a transport in the underlying net/http Client. func NewWithTransport(transport http.RoundTripper) *Client { if transport == nil { panic("transport must be non-nil") @@ -46,6 +48,9 @@ func (c *Client) WithBaseTransport(base http.RoundTripper) *Client { return c } +// WithDefaultHeaders adds the given name-value pairs as request headers on every Request. +// Headers can be added or overridden using the WithHeaders functional option parameter +// on a per-request basis. func (c *Client) WithDefaultHeaders(headers map[string]string) *Client { if c.defaultHeaders == nil { c.defaultHeaders = make(map[string]string) @@ -101,6 +106,7 @@ func (c *Client) prepareRequest(ctx context.Context, method string, rawURL strin return req, nil } +// Head sends a HEAD Request. func (c *Client) Head(ctx context.Context, url string, parameters ...RequestParameter) (*http.Response, error) { req, err := c.prepareRequest(ctx, http.MethodHead, url, nil, parameters...) if err != nil { diff --git a/examples/example_client_json_api_test.go b/examples/example_client_json_api_test.go new file mode 100644 index 0000000..c4adbb4 --- /dev/null +++ b/examples/example_client_json_api_test.go @@ -0,0 +1,35 @@ +package examples + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/georgepsarakis/go-httpclient/examples/githubsdk" +) + +func Example() { + sdk := githubsdk.New() + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + panicOnError(err) + + m, err := json.MarshalIndent(user, "", " ") + panicOnError(err) + + fmt.Println(string(m)) + // Output: + //{ + // "id": 963304, + // "bio": "Software Engineer", + // "blog": "https://controlflow.substack.com/", + // "created_at": "2011-08-06T16:57:12Z", + // "login": "georgepsarakis", + // "name": "George Psarakis" + //} +} + +func panicOnError(err error) { + if err != nil { + panic(err) + } +} diff --git a/examples/example_client_json_api_testing_test.go b/examples/example_client_json_api_testing_test.go new file mode 100644 index 0000000..ac384bb --- /dev/null +++ b/examples/example_client_json_api_testing_test.go @@ -0,0 +1,62 @@ +package examples_test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/georgepsarakis/go-httpclient" + "github.com/georgepsarakis/go-httpclient/examples/githubsdk" + "github.com/georgepsarakis/go-httpclient/httpassert" + "github.com/georgepsarakis/go-httpclient/httptesting" +) + +func TestGitHubSDK_GetUserByUsername(t *testing.T) { + testClient := httptesting.NewClient(t) + sdk := githubsdk.NewWithClient(testClient.Client) + testClient, err := testClient.WithBaseURL("https://test-api-github-com") + require.NoError(t, err) + + testClient. + NewMockRequest( + http.MethodGet, + "https://test-api-github-com/users/georgepsarakis", + httpclient.WithHeaders(map[string]string{ + "x-github-api-version": "2022-11-28", + "accept": "application/vnd.github+json", + })). + RespondWithJSON(http.StatusOK, ` + { + "id": 963304, + "bio": "Test 123", + "blog": "https://test.blog/", + "created_at": "2025-09-16T16:57:12Z", + "login": "georgepsarakis", + "name": "Test Name" + }`).Register() + + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + require.NoError(t, err) + + httpassert.PrintJSON(t, user) + // Output: + //{ + // "id": 963304, + // "bio": "Test 123", + // "blog": "https://test.blog/", + // "created_at": "2025-09-16T16:57:12Z", + // "login": "georgepsarakis", + // "name": "Test Name" + //} + require.Equal(t, githubsdk.User{ + ID: 963304, + Bio: "Test 123", + Blog: "https://test.blog/", + CreatedAt: time.Date(2025, time.September, 16, 16, 57, 12, 0, time.UTC), + Login: "georgepsarakis", + Name: "Test Name", + }, user) +} diff --git a/examples/example_net_http_client_json_api_test.go b/examples/example_net_http_client_json_api_test.go new file mode 100644 index 0000000..b7d4881 --- /dev/null +++ b/examples/example_net_http_client_json_api_test.go @@ -0,0 +1,129 @@ +package examples_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + "time" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" + + "github.com/georgepsarakis/go-httpclient/httpassert" +) + +type GitHubSDK struct { + Client *http.Client + DefaultHeaders http.Header + BaseURL string +} + +func New() GitHubSDK { + client := http.DefaultClient + return NewWithClient(client) +} + +func NewWithClient(c *http.Client) GitHubSDK { + headers := http.Header{} + headers.Set("X-GitHub-Api-Version", "2022-11-28") + headers.Set("Accept", "application/vnd.github+json") + return GitHubSDK{Client: c, DefaultHeaders: headers, BaseURL: "https://api.github.com"} +} + +type User struct { + ID int `json:"id"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt time.Time `json:"created_at"` + Login string `json:"login"` + Name string `json:"name"` +} + +// GetUserByUsername retrieves a user based on their public username. +// See https://docs.github.com/en/rest/users/users +func (g GitHubSDK) GetUserByUsername(ctx context.Context, username string) (User, error) { + path, err := url.JoinPath("/users", username) + if err != nil { + return User{}, err + } + fullURL := fmt.Sprintf("%s%s", g.BaseURL, path) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) + if err != nil { + return User{}, err + } + req.Header = g.DefaultHeaders.Clone() + resp, err := g.Client.Do(req) + if err != nil { + return User{}, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return User{}, err + } + u := User{} + if err := json.Unmarshal(b, &u); err != nil { + return User{}, err + } + return u, nil +} + +func TestGitHubSDK_NetHTTP_GetUserByUsername(t *testing.T) { + var err error + mt := httpmock.NewMockTransport() + testClient := &http.Client{ + Transport: mt, + } + sdk := NewWithClient(testClient) + sdk.BaseURL = "https://test-api-github-com" + + responder, err := httpmock.NewJsonResponder(http.StatusOK, json.RawMessage(` + { + "id": 963304, + "bio": "Test 123", + "blog": "https://test.blog/", + "created_at": "2025-09-16T16:57:12Z", + "login": "georgepsarakis", + "name": "Test Name" + }`)) + require.NoError(t, err) + defaultHeaderMatcher := func(req *http.Request) bool { + return req.Header.Get("Accept") == "application/vnd.github+json" && + req.Header.Get("X-GitHub-Api-Version") == "2022-11-28" + } + mt.RegisterMatcherResponder(http.MethodGet, + "https://test-api-github-com/users/georgepsarakis", + httpmock.NewMatcher("GetUserByUsername", func(req *http.Request) bool { + if !defaultHeaderMatcher(req) { + return false + } + return true + }), + responder) + + user, err := sdk.GetUserByUsername(context.Background(), "georgepsarakis") + require.NoError(t, err) + + httpassert.PrintJSON(t, user) + // Output: + //{ + // "id": 963304, + // "bio": "Test 123", + // "blog": "https://test.blog/", + // "created_at": "2025-09-16T16:57:12Z", + // "login": "georgepsarakis", + // "name": "Test Name" + //} + require.Equal(t, User{ + ID: 963304, + Bio: "Test 123", + Blog: "https://test.blog/", + CreatedAt: time.Date(2025, time.September, 16, 16, 57, 12, 0, time.UTC), + Login: "georgepsarakis", + Name: "Test Name", + }, user) +} diff --git a/examples/githubsdk/githubsdk.go b/examples/githubsdk/githubsdk.go new file mode 100644 index 0000000..77786d8 --- /dev/null +++ b/examples/githubsdk/githubsdk.go @@ -0,0 +1,54 @@ +package githubsdk + +import ( + "context" + "net/url" + "time" + + "github.com/georgepsarakis/go-httpclient" +) + +type GitHubSDK struct { + Client *httpclient.Client +} + +func New() GitHubSDK { + client := httpclient.New() + return NewWithClient(client) +} + +func NewWithClient(c *httpclient.Client) GitHubSDK { + c.WithDefaultHeaders(map[string]string{ + "X-GitHub-Api-Version": "2022-11-28", + "Accept": "application/vnd.github+json", + }) + c, _ = c.WithBaseURL("https://api.github.com") + return GitHubSDK{Client: c} +} + +type User struct { + ID int `json:"id"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt time.Time `json:"created_at"` + Login string `json:"login"` + Name string `json:"name"` +} + +// GetUserByUsername retrieves a user based on their public username. +// See https://docs.github.com/en/rest/users/users +func (g GitHubSDK) GetUserByUsername(ctx context.Context, username string) (User, error) { + path, err := url.JoinPath("/users", username) + if err != nil { + return User{}, err + } + resp, err := g.Client.Get(ctx, path) + if err != nil { + return User{}, err + } + u := User{} + if err := httpclient.DeserializeJSON(resp, &u); err != nil { + return u, err + } + return u, nil +} diff --git a/httpassert/response.go b/httpassert/response.go index 38e63c6..37bc833 100644 --- a/httpassert/response.go +++ b/httpassert/response.go @@ -3,8 +3,8 @@ package httpassert import ( "bytes" "io" + "mime" "net/http" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -27,7 +27,8 @@ func ResponseEqual(t *testing.T, actual, expected *http.Response) { actual.Body.Close() // Restore the body stream in order to allow multiple assertions actual.Body = io.NopCloser(bytes.NewBuffer(actualBody)) - if strings.HasPrefix(actual.Header.Get("Content-Type"), "application/json") { + mediatype, _, err := mime.ParseMediaType(actual.Header.Get("Content-Type")) + if mediatype == "application/json" { assert.JSONEq(t, string(expectedBody), string(actualBody)) } else { assert.Equal(t, string(expectedBody), string(actualBody)) @@ -36,6 +37,8 @@ func ResponseEqual(t *testing.T, actual, expected *http.Response) { if expected.Request != nil { assert.Equal(t, expected.Request.URL, actual.Request.URL) assert.Equal(t, expected.Request.Method, actual.Request.Method) + assert.Equal(t, expected.Request.Proto, actual.Request.Proto) + assert.Equal(t, expected.Request.Header, actual.Request.Header) } } diff --git a/httpassert/response_test.go b/httpassert/response_test.go index 5cc8bb3..922dde3 100644 --- a/httpassert/response_test.go +++ b/httpassert/response_test.go @@ -1,7 +1,9 @@ package httpassert import ( + "io" "net/http" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -44,6 +46,48 @@ func TestResponseEqual(t *testing.T) { }, want: assert.False, }, + { + name: "body payload matches", + args: args{ + t: &testing.T{}, + actual: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(`{"hello": "world"}`)), + }, + expected: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(`{"hello": "world"}`)), + }, + }, + want: assert.False, + }, + { + name: "body payload does not match", + args: args{ + t: &testing.T{}, + actual: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(`{"hello": "worl"}`)), + }, + expected: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(`{"hello": "world"}`)), + }, + }, + want: assert.True, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/httptesting/client.go b/httptesting/client.go index 4bc6eb7..47a0312 100644 --- a/httptesting/client.go +++ b/httptesting/client.go @@ -61,6 +61,11 @@ func (c *Client) WithBaseURL(baseURL string) (*Client, error) { return c, nil } +func (c *Client) WithDefaultHeaders(headers map[string]string) *Client { + c.Client = c.Client.WithDefaultHeaders(headers) + return c +} + // HTTPMock exposes the httpmock.MockTransport instance for advanced usage. func (c *Client) HTTPMock() *httpmock.MockTransport { return c.mock diff --git a/httptesting/client_test.go b/httptesting/client_test.go index dd4513d..70a906d 100644 --- a/httptesting/client_test.go +++ b/httptesting/client_test.go @@ -31,6 +31,7 @@ func TestClient_Get(t *testing.T) { Method: http.MethodGet, Header: reqHeaders, URL: httpassert.URLFromString(t, requestURL+"?test=1"), + Proto: "HTTP/1.1", }, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"name": "hello", "surname": "world"}`)), @@ -52,16 +53,18 @@ func TestClient_Head(t *testing.T) { httpclient.WithQueryParameters(map[string]string{"test": "1"})) require.NoError(t, err) reqHeaders := http.Header{} - reqHeaders.Set("Accept", "application/json") + reqHeaders.Set("Content-Type", "application/json") httpassert.ResponseEqual(t, resp, &http.Response{ StatusCode: http.StatusOK, Request: &http.Request{ Method: http.MethodHead, Header: reqHeaders, URL: httpassert.URLFromString(t, requestURL+"?test=1"), + Proto: "HTTP/1.1", }, Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(strings.NewReader(`{"name": "hello", "surname": "world"}`)), }) - httpassert.SuccessfulJSONResponseEqual(t, resp, []byte(`{"name": "hello", "surname": "world"}`)) + httpassert.ResponseEqual(t, resp, &http.Response{ + StatusCode: http.StatusOK, + }) }