httpmock

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2024 License: MIT Imports: 15 Imported by: 0

README

httpmock - testify for HTTP Requests

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.

Features

SafeReadBody

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.

httpmock.Mock
On

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()
AnyMethod

Use httpmock.AnyMethod to indicate the expected request can contain any valid HTTP method.

Mock.On(httpmock.AnyMethod, "/some/path", nil)
AnyBody

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)
httpmock.Request
Matches

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.

Times, Once, Twice

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.

Respond, RespondOK, RespondNoContent

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.

RespondUsing

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)
httpmock.Response
Header

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
NotRecoverable, IsRecoverable

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.

Installation

To install httpmock, use go get:

go get github.com/shawalli/httpmock

Troubleshooting

Nil pointer dereference while reading http.Request

If 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.

Todo

  • Extend httptest.Server to provide a single implementation
  • Request URL matcher
  • Request matcher functions
  • Request header matching (implemented with matcher functions feature)
  • Response custom function

License

This project is licensed under the terms of the MIT license.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrReadBody = errors.New("error reading body")

	AnyMethod = "httpmock.AnyMethod"
	AnyBody   = []byte("httpmock.AnyBody")
)
View Source
var ErrWriteReturnBody = errors.New("error writing return body")

Functions

func SafeReadBody added in v0.4.0

func SafeReadBody(received *http.Request) ([]byte, error)

SafeReadBody reads the body of a http.Request and resets the http.Request's body so that it may be read again afterward.

Types

type Mock

type Mock struct {
	// Represents the requests that are expected to be received.
	ExpectedRequests []*Request

	// Holds the requests that were made to a mocked handler or server.
	Requests []Request
	// contains filtered or unexported fields
}

Mock is the workhorse used to track activity of a server's requesst. For an example of its usage, refer to the README.

func (*Mock) AssertExpectations

func (m *Mock) AssertExpectations(t mock.TestingT) bool

AssertExpectations assert that everything specified with Mock.On and Request.Respond was in fact requested as expected. Request's may have occurred in any order.

func (*Mock) AssertNotRequested

func (m *Mock) AssertNotRequested(t mock.TestingT, method string, path string, body []byte) bool

AssertRequested asserts that the request was not received.

func (*Mock) AssertNumberOfRequests

func (m *Mock) AssertNumberOfRequests(t mock.TestingT, method string, path string, expectedRequests int) bool

AssertNumberOfRequests asserts that the request was made expectedRequests times.

This assertion behaves a bit differently than other assertions. There are a few parts of the request that are ignored when calculating, including:

  • URL username/password information
  • URL query parameters
  • URL fragment

func (*Mock) AssertRequested

func (m *Mock) AssertRequested(t mock.TestingT, method string, path string, body []byte) bool

AssertRequested asserts that the request was received.

func (*Mock) On

func (m *Mock) On(method string, URL string, body []byte) *Request

On starts a description of an expectation of the specified Request being received.

Mock.On(http.MethodDelete, "/some/path/1234")

func (*Mock) Requested

func (m *Mock) Requested(received *http.Request) *Response

Requested tells the mock that a http.Request has been received and gets a response to return. Panics if the request is unexpected (i.e. not preceded by appropriate Mock.On calls).

func (*Mock) Test

func (m *Mock) Test(t mock.TestingT) *Mock

Test sets the test struct variable of the Mock object.

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request represents a http.Request and is used for setting expectations, as well as recording activity.

func (*Request) Matches added in v0.3.0

func (r *Request) Matches(matchers ...RequestMatcher) *Request

Matches adds one or more RequestMatcher's to the Request. RequestMatcher's are called in FIFO order after the HTTP method, URL, and body have been matched.

func queryAtLeast(key string, minValue int) RequestMatcher {
	fn := func(received *http.Request) (output string, differences int) {
		v := received.URL.Query().Get(key)
		if v == "" {
			output = fmt.Sprintf("FAIL:  queryAtLeast: (Missing) ((%s)) != %s", received.URL.Query().Encode(), key)
			differences = 1
			return
		}
		val, err := strconv.Atoi(v)
		if err != nil {
			output = fmt.Sprintf("FAIL:  queryAtLeast: %s value %q unable to coerce to int", key, v)
			differences = 1
			return
		}
		if val < minValue {
			output = fmt.Sprintf("FAIL:  queryAtLeast: %d < %d", val, minValue)
			differences = 1
			return
		}
		output = fmt.Sprintf("PASS:  queryAtLeast: %d >= %d", val, minValue)
		return
	}

	return fn
}

Mock.On(http.MethodGet, "/some/path/1234", nil).Matches(queryAtLeast("page", 2))

func (*Request) Once

func (r *Request) Once() *Request

Once indicates that the Mock should only return the response once.

Mock.On(http.MethodDelete, "/some/path/1234").Once()

func (*Request) Respond

func (r *Request) Respond(statusCode int, body []byte) *Response

Respond specifies the response arguments for the expectation.

Mock.On(http.GetMethod, "/some/path").Respond(http.StatusInternalServerError, nil)

func (*Request) RespondNoContent

func (r *Request) RespondNoContent() *Response

RespondNoContent is a convenience method that sets the status code as 204.

Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent()

func (*Request) RespondOK

func (r *Request) RespondOK(body []byte) *Response

RespondOK is a convenience method that sets the status code as 200 and the provided body.

Mock.On(http.GetMethod, "/some/path").RespondOK([]byte(`{"foo", "bar"}`))

func (*Request) RespondUsing added in v0.5.0

func (r *Request) RespondUsing(writer ResponseWriter) *Response

RespondUsing overrides the Request.Respond functionality by allowing a custom writer to be invoked instead of the typical writing functionality.

Note: The `writer` is responsible for the entire response, including headers, status code, and body.

func (*Request) String

func (r *Request) String() string

String computes a formatted string representing a Request.

func (*Request) Times

func (r *Request) Times(i int) *Request

Times indicates that the Mock should only return the indicated number of times.

Mock.On(http.MethodDelete, "/some/path/1234").Times(5)

func (*Request) Twice

func (r *Request) Twice() *Request

Twice indicates that the Mock should only return the response twice.

Mock.On(http.MethodDelete, "/some/path/1234").Twice()

type RequestMatcher added in v0.3.0

type RequestMatcher func(received *http.Request) (output string, differences int)

RequestMatcher is used by the Request.Matches method to match a http.Request.

type Response

type Response struct {
	// contains filtered or unexported fields
}

Response hold the parts of the response that should be returned.

func (*Response) Header

func (r *Response) Header(key string, value string, values ...string) *Response

Header sets the value or values for a response header. Any prior values that have already been set for a header with the same key will be overridden.

func (*Response) On

func (r *Response) On(method string, path string, body []byte) *Request

On chains a new expectation description onto the grandparent Mock. This allows syntax like:

Mock.
	On(http.MethodPost, "/some/path").RespondOk([]byte(`{"id": "1234"}`)).
	On(http.MethodDelete, "/some/path/1234").RespondNoContent().
	On(http.MethodDelete, "/some/path/1234").Respond(http.StatusNotFound, nil)

func (*Response) Once

func (r *Response) Once() *Request

Once is a convenience method which indicates that the grandparent Mock should only expect the parent request once.

Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent().Once()

func (*Response) Times

func (r *Response) Times(i int) *Request

Times is a convenience method which indicates that the grandparent Mock should only expect the parent request the indicated number of times.

Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent().Times(5)

func (*Response) Twice

func (r *Response) Twice() *Request

Twice is a convenience method which indicates that the grandparent Mock should only expect the parent request twice.

Mock.On(http.MethodDelete, "/some/path/1234").RespondNoContent().Twice()

func (*Response) Write

func (r *Response) Write(w http.ResponseWriter, req *http.Request) (int, error)

Write the response to the provided http.ResponseWriter. The number of bytes successfully written to the http.ResponseWriter are returned, as well as any errors.

Note: If Request.RespondUsing was previously called, all response configurations are ignored except for the provided custom ResponseWriter.

type ResponseWriter added in v0.5.0

type ResponseWriter func(w http.ResponseWriter, r *http.Request) (int, error)

ResponseWriter writes a http.Response and returns the number of bytes written and whether or not the operation encountered an error.

*http.Request is provided as an argument so that the response writer can use information from the request when crafting the response. If the ResponseWriter is static, the *http.Request may be safely ignored.

type Server

type Server struct {
	*httptest.Server

	Mock *Mock
	// contains filtered or unexported fields
}

Server simplifies the orchestration of a Mock inside a handler and server. It wraps the stdlib httptest.Server implementation and provides a handler to log requests and write configured responses.

func NewServer

func NewServer() *Server

NewServer creates a new Server and associated Mock.

func NewServerWithConfig added in v0.6.0

func NewServerWithConfig(cfg ServerConfig) *Server

func (*Server) IsRecoverable

func (s *Server) IsRecoverable() bool

IsRecoverable returns whether or not the Server is considered recoverable.

func (*Server) NotRecoverable added in v0.6.0

func (s *Server) NotRecoverable() *Server

NotRecoverable sets a Server as not recoverable, so that panics are allowed to propagate to the main process. With the default handler, panics are caught and printed to stdout, with a final 404 returned to the client.

404 was chosen rather than 500 due to panics almost always occurring when a matching Request cannot be found. However, custom handlers can choose to implement their recovery mechanism however they would like, using the Server.IsRecoverable method to access this value.

func (*Server) On

func (s *Server) On(method string, URL string, body []byte) *Request

On is a convenience method to invoke the Mock.On method.

Server.On(http.MethodDelete, "/some/path/1234")

type ServerConfig added in v0.6.0

type ServerConfig struct {
	// Create TLS-configured server
	TLS bool

	// Custom server handler
	Handler http.HandlerFunc
}

ServerConfig contains settings for configuring a Server. It is used with NewServerWithConfig. For default behavior, use NewServer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL