This package provides a mocking interface in the spirit of stretchr/testify/mock for HTTP requests.
package yours
import (
"net/http"
"testing"
"github.com/shawalli/httpmock"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// Setup default test server and handler to log requests and return expected responses.
// You may also create your own test server, handler, and mock to manage this.
ts := httpmock.NewServer()
defer ts.Close()
// Configure request mocks
expectBearerToken := func(received *http.Request) (output string, differences int) {
if _, ok := received.Header["Authorization"]; !ok {
output = "FAIL: missing header Authorization"
differences = 1
return
}
val := received.Header.Get("Authorization")
if !strings.HasPrefix(val, "Bearer ") {
output = fmt.Sprintf("FAIL: header Authorization: %q != Bearer", val)
differences = 1
return
}
output = fmt.Sprintf("PASS: header Authorization: %q == Bearer", val)
return
}
ts.On(http.MethodPatch, "/foo/1234", []byte(`{"bar": "baz"}`)).
Matches(expectBearerToken).
RespondOK([]byte(`Success!`)).
Once()
// Test application code
tc := ts.Client()
req, err := http.NewRequest(
http.MethodPatch,
fmt.Sprintf("%s/foo/1234", ts.URL),
io.NopCloser(strings.NewReader(`{"bar": "baz"}`)),
)
if err != nil {
t.Fatalf("Failed to create request! %v", err)
}
req.Header.Add("Authorization", "Bearer jkel3450d")
resp, err := tc.Do(req)
if err != nil {
t.Fatalf("Failed to do request! %v", err)
}
// Assert application expectations
assert.Equal(t, http.StatusOK, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body! %v", err)
}
assert.Equal(t, "Success!", string(respBody))
// Assert httpmock expectations
ts.Mock.AssertExpectations(t)
ts.Mock.AssertNumberOfRequests(t, http.MethodPatch, "/foo/1234", 1)
}You can also use Mock directly and implement your own test server. To do so,
you should wire up your handler so that the request is passed to
Mock.Requested(r), and respond using the returned Response's Write(w)
method.
httpmock.SafeReadBody will read a http.Request.Body and resets the http.Request.Body with a fresh io.Reader so
that subsequent logic may also read the body.
The On() method is the primary mechanism for configuring mocks. It is primarily designed for httpmock.Mock. To support common chaining patterns found in both httpmock and testify/mock, the On() method may also be found on common structs, such as httpmock.Server and httpmock.Response. In these cases, the On() method is just a convenience wrapper to register an expected request against the underlying httpmock.Mock object.
Mock.On(http.MethodPost, "/some/path/1234").RespondOK()
Server.On(http.MethodPost, "/some/path/1234").RespondOK()
Mock.
On(http.MethodPost, "/some/path/1234").
RespondOK().
On(http.MethodDelete, "/some/path/1234").
RespondNoContent()Use httpmock.AnyMethod to indicate the expected request can contain any valid HTTP method.
Mock.On(httpmock.AnyMethod, "/some/path", nil)Use httpmock.AnyBody to indicate the expected request can contain any body, or no body at all.
Mock.On(http.MethodPost, "/some/path/1234", httpmock.AnyBody)Use httpmock.Request.Matches() to perform more complex matching for an expected request. For example, expect that a request should have an Authorization header that uses a bearer token.
expectBearerToken := func(received *http.Request) (output string, differences int) {
if _, ok := received.Header["Authorization"]; !ok {
output = "FAIL: missing header Authorization"
differences = 1
return
}
val := received.Header.Get("Authorization")
if !strings.HasPrefix(val, "Bearer ") {
output = fmt.Sprintf("FAIL: header Authorization: %q != Bearer", val)
differences = 1
return
}
output = fmt.Sprintf("PASS: header Authorization: %q == Bearer", val)
return
}
Mock.On(http.MethodPost, "/some/path/1234", nil).Matches(expectBearerToken)On Formatting: For readability, try to conform to the following pattern when formatting your output:
FAIL: <actual> != <expected>
PASS: <actual> == <expected>
The diff formatting will take care of tabs, newlines, and match-indices for you, so please do not include those formatters.
Just like testify/mock, httpmock assumes that an expected request may be matched in perpetuity by default. This
assumption may be altered with the httpmock.Request.Times() method. Times() takes an integer that indicates the
number of times an expected request should match. After the configured number of times, an expected request will not
match even if it would match otherwise.
Additionally, two convenience methods are available to simplify common configurations: Once() and Twice(). They
behave as one would expect.
Mock.On(http.MethodDelete, "/some/path/1234").Once().RespondNoContent()
Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent().Once()Note: To support chaining, these methods may also be found on the httpmock.Response struct as convenience wrappers into the underlying httpmock.Request object.
httpmock provides a basic method to register desired responses to a request with the httpmock.Request.Respond()
method. It takes a status code and response body.
Additionally, two convenience methods are available to simplify common patterns:
RespondOK()- This method responds with a 200 status code and allows for a custom body.RespondNoContent()- This responds with a 204 status code and does not take a body, since 204 indicates that the response contains no content.
Mock.On(http.MethodPost, "/some/path", []byte("spam")).RespondOK([]byte(`{"id": "1234"}`))
Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent()
Mock.On(http.MethodGet, "/some/path/1234").Respond(http.StatusNotFound, nil)
Mock.On(http.MethodGet, "/some/path/1234").Respond(http.StatusNotFound, []byte(`{"error": "path resource not found"}`))In the future, more convenience methods may be added if they are common, clearly defined, and enhance the readability and simplification of the mock response configuration.
If more complex functionality is needed than Respond can provide, httpmock allows for custom response
implementations with this method. If RespondUsing is called, all of the other Respond configurations
are ignored.
// respWriter calculates the count based on the page and limit and returns these values in the response.
respWriter := func(w http.ResponseWriter, r *http.Request) (int, error) {
v := r.URL.Query().Get("limit")
limit := 10
if v != "" {
limit, _ = strconv.Atoi(v)
}
v = r.URL.Query().Get("page")
var count, page int
if v != "" {
page, _ := strconv.Atoi(v)
count = limit * page
}
w.WriteHeader(http.StatusOK)
return w.Write([]byte(`{"count": %d, "page": %d, "limit": %d, "result": {...}}`, count, page, limit))
}
Mock.On(http.MethodGet, "/some/path/1234?page=3&limit=20", nil).RespondUsing(respWriter)Use httpmock.Response.Header() to set headers on the response. Multiple values may be passed for the header's value.
However, multiple invocations against the same header will overwrite previous values with the most recent values.
Mock.On(http.MethodGet, "/some/path", nil).RespondOK([]byte(`{"id": "1234"}`)).Header("next", "abcd")httpmock.Server is a glorified version of httptest.Server with a default handler. With both server types, the
server runs as a goroutine. The default behavior is to log the panic details and recover from it. However, an
implementation can set NotRecoverable() to indicate to the default or custom handler that an unmatched request
should cause the server to panic outside of the server goroutine and into the main process.
If writing a custom handler, the handler should react to a panic based on the server's IsRecoverable() response.
To install httpmock, use go get:
go get github.com/shawalli/httpmockIf using httpmock.Server or httptest.Server, a http.Request with no body must use http.NoBody instead of
nil. This is due to the fact that the test server is not actually sending and receiving a request, but rather mocking
a request. Using http.NoBody indicates to the net/http package that the request has no body but is still a valid
io.Reader.
- Extend
httptest.Serverto provide a single implementation - Request matcher functions
[ ] Request URL matcher(can be implemented with matcher functions feature)[ ] Request header matching(can be implemented with matcher functions feature)- Response custom function
This project is licensed under the terms of the MIT license.