httpregistry

package module
v0.0.0-...-40eba9f Latest Latest
Warning

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

Go to latest
Published: May 25, 2025 License: MIT Imports: 10 Imported by: 0

README

HTTPRegistry

HTTPRegistry is a tiny package designed to simplifying building configurable httptest servers. httptest is an incredibly powerful package that can be used to test the behavior of code that makes http calls. Unfortunately when the chain of calls to be tested is complex, setting up a mock server gets complicated and full of boilerplate test. This library is defined to take care of the boilerplate and let you focus on what matters in your tests.

Basic concepts

In a nutshell this library allows you to create a registry on which responses to requests can be registered. Then this registry can be used to instantiate a httptest server that can respond to http requests. The library then takes care of checking that all the responses are used and that not too many calls happen.

package main

import (
	"net/http"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestHttpRegistryWorks(t *testing.T) {
    // 1. Create the registry and defer the check that all responses
    //    that we will create are used.
    //    t is used to fail the test if the deferred check fails.
	registry := httpregistry.NewRegistry(t)
	defer registry.CheckAllResponsesAreConsumed()

    // 2. Add request to the registry
	registry.AddMethodAndURL(http.MethodGet, "/users")

    // 3. Create the server
	server := registry.GetServer()
	defer server.Close()

    // 4. Make calls, since we registered a single call then if we would call "/users" again it would fail
	response, err := http.Get(server.URL + "/users")
	if err != nil {
		t.Errorf("executing request failed: %v", err)
	}
	if response.StatusCode != 200 {
		t.Errorf("unexpected status code %v", response.StatusCode)
	}
}
Requests/Responses

The library provides various helper functions to make the process of attaching a response to a request easier. In the most general form it uses two types

  • Request that defines the expected response to match
  • Response that defines the response to be returned if a match happens

a request can be matched via the following

  • A (method, exact path) combination, like GET /users
  • A (method, regex) combination, like GET /users/*

plus additionally other constrains can be places on the matching

  • It can be requested the request contains some headers like Accept: text/html.

Once a request is matched the corresponding Response is used to determine what the server should return. Currently the library allows to set

  • Status code
  • Body
  • Headers
Retrieving matching requests

Sometimes it is useful to retrieve the http.Request that matched a HttpRegistry.Request. For example to check if the body if a POST/PUT request is what we expect or similar. One can do that via three functions

  • registry.GetMatchesForRequest(request)
  • registry.GetMatchesForURL(url)
  • registry.GetMatchesURLAndMethod(url, method)
Infinite responses

A Response is consumed when a match happen, this is by design so that it is possible to test that the expected number of calls happens, but sometimes one does not really care about how many calls are made and just wants to mock a http call away. This is possible via httpregistry.AddInfiniteResponse(response)

import (
	"net/http"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestAddInfiniteResponse(t *testing.T) {
	nbCalls := 100

	// 1. Create the registry and defer the check that all responses
	//    that we will create are used.
	//    t is used to fail the test if the deferred check fails.
	registry := httpregistry.NewRegistry(t)

	// 2. Add an AddInfiniteResponse,
	// 	  by default Response are consumed when they match but
	//    InfiniteResponse are not so they can be matched forever
	registry.AddInfiniteResponse(
		httpregistry.NewResponse(),
	)

	// 3. Create the server
	server := registry.GetServer()
	defer server.Close()
	client := http.Client{}

	// 4. Make calls and check assertions
	var urls []string
	for range nbCalls {
		url := generateRandomString(10)

		res, err := client.Get(server.URL + "/" + url)
		if err != nil {
			t.Errorf("executing request failed: %v", err)
		}

		if res.StatusCode != 200 {
			t.Errorf("unexpected status code %v", res.StatusCode)
		}

		urls = append(urls, url)
	}

	requests := registry.GetMatchesForRequest(httpregistry.DefaultRequest)
	if len(requests) != nbCalls {
		t.Errorf("the number of requests (%d) does not match the number of calls (%d)", len(requests), nbCalls)
	}

	for i, r := range requests {
		if r.URL.Path != "/"+urls[i] {
			t.Errorf("the request path (%s) does not match the expected path (%s)", r.URL.Path, "/"+urls[i])
		}
	}
}
Custom responses

Sometimes the standard Response from the package is not enough, suppose that you want to return a different value depending on the request, so for example you want to match an ID in the path or something similar. This is not possible with a Response since it does not allow to interact with the http.Request that is coming in. To solve this problem this package provides a CustomResponse type that allows you to interact with both the http.Request and the http.ResponseWriter.

package main

import (
	"encoding/json"
	"io"
	"net/http"
	"regexp"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestCustomRequestWorks(t *testing.T) {
    // 1. Create the registry and defer the check that all responses
	//    that we will create are used.
	//    t is used to fail the test if the deferred check fails.
	registry := httpregistry.NewRegistry(t)
	defer registry.CheckAllResponsesAreConsumed()

	// 2. Create a CustomResponse, all functions that accept Response also accept CustomResponse
	mockResponse := httpregistry.NewCustomResponse(func(w http.ResponseWriter, r *http.Request) {
		regexUser := regexp.MustCompile(`/users/(?P<userID>.+)/address$`)
		if regexUser.MatchString(r.URL.Path) {
			matches := regexUser.FindStringSubmatch(r.URL.Path)
			userID := matches[regexUser.SubexpIndex("userID")]
			body := map[string]string{"user_id": userID}
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(&body)
			return
		}
	})
	// Optional add a name to the CustomResponse so it is easier to debug, by default they get "custom response 1,2,3" as name
	mockResponse = mockResponse.WithName("match on user ID")
	registry.AddResponse(mockResponse)

	// 3. Create the server
	server := registry.GetServer()
	defer server.Close()

	// 4. Make calls and check assertions
	response, err := http.Get(server.URL + "/users/12/address")
	if err != nil {
		t.Errorf("executing request failed: %v", err)
	}
	if response.StatusCode != 200 {
		t.Errorf("unexpected status code %v", response.StatusCode)
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		t.Errorf("reading body failed: %v", err)
	}

	expectedBody := "{\"user_id\":\"12\"}\n"
	if string(body) != expectedBody {
		t.Errorf("body does not match expected body")
	}
}

Investigate failed tests

The library tries to help as much as possible in debugging why a test has failed. To achieve this it will

  1. Fail a test if
    1. It is impossible to reply to a request
    2. registry.CheckAllResponsesAreConsumed() is called but not all the requests are consumed
  2. In case if it is impossible to reply to a request it will report in the body of the response why it failed
  3. Provide a httpregistry.NewMockTestingT() that can be passed in place of *testing.T so that test failures can be better analyzed
Investigate if a test fails to consume all requests
package main

import (
	"net/http"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestHowToInvestigateFailingTest(t *testing.T) {
	// 1. Setup a mock for testing.T that we control and can access later
	mockT := httpregistry.NewMockTestingT()

	// 2. Setup registry and the requests
	registry := httpregistry.NewRegistry(mockT)
	registry.AddMethodAndURL(http.MethodGet, "/foo")
	registry.AddMethodAndURL(http.MethodDelete, "/bar")
	registry.AddRequestWithResponse(
		httpregistry.DefaultRequest,
		httpregistry.NewResponse().WithName("My beautiful response"),
	)
	registry.AddRequestWithResponse(
		httpregistry.DefaultRequest,
		httpregistry.NewCustomResponse(func(w http.ResponseWriter, r *http.Request) {}).WithName("My beautiful custom response"),
	)

	// 3. No call happens but we assert that all calls were consumed
	registry.CheckAllResponsesAreConsumed()

	// 4. Let us check that mockT contains useful information
	if len(mockT.Messages) != 4 {
		t.Errorf("There should be 4 uncalled request but I found only %d", len(mockT.Messages))
	}

	if !slices.Contains(mockT.Messages, "request mock request #1 has httpregistry.OkResponse as unused response") {
		t.Error("request mock request #1 has httpregistry.OkResponse as unused response should be in the slice but it is not")
	}
	if !slices.Contains(mockT.Messages, "request mock request #2 has httpregistry.OkResponse as unused response") {
		t.Error("request mock request #2 has httpregistry.OkResponse as unused response should be in the slice but it is not")
	}
	if !slices.Contains(mockT.Messages, "request httpregistry.DefaultRequest has My beautiful response as unused response") {
		t.Error("request httpregistry.DefaultRequest has My beautiful response as unused response should be in the slice but it is not")
	}
	if !slices.Contains(mockT.Messages, "request httpregistry.DefaultRequest has My beautiful custom response as unused response") {
		t.Error("request httpregistry.DefaultRequest has My beautiful custom response as unused response should be in the slice but it is not")
	}
}
Investigate why a test fails when calling
package main

import (
	"net/http"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestWeCanInvestigateWhyATestFails(t *testing.T) {
	// 1. Setup a mock for testing.T that we control and can access later
	mockT := httpregistry.NewMockTestingT()

	// 2. Setup registry and the requests
	registry := httpregistry.NewRegistry(mockT)
	registry.AddMethodAndURL(http.MethodGet, "/foo")

	// 3. Call Twice a route with only one response
	url := registry.GetServer().URL
	client := http.Client{}

	// 3a. First call works
	firstResponse, err := client.Get(url + "/foo")
	if err != nil {
		t.Errorf("Unexpected error in first request: %s", err)
	}
	if firstResponse.StatusCode != 200 {
		t.Errorf("Unexpected status code for first response, I was expecting 200 we got %d", firstResponse.StatusCode)
	}

	// 3b. Second call fails
	secondResponse, err := client.Get(url + "/foo")
	if err != nil {
		t.Errorf("Unexpected error in second request: %s", err)
	}
	if secondResponse.StatusCode != 500 {
		t.Errorf("Unexpected status code for second response, I was expecting 500 we got %d", secondResponse.StatusCode)
	}

	// 4. The test was failed
	if mockT.HasFailed != true {
		t.Errorf("mockT.HasFailed should be true, but it was %t", mockT.HasFailed)
	}

	// 5. The body of the call tells us why it failed
	bodyBytes, err := io.ReadAll(secondResponse.Body)
	if err != nil {
		t.Errorf("Decoding second response body failed: %s", err)
	}
	body := string(bodyBytes)
	if body != "mock request #1 missed because the route matches but there was no response available" {
		t.Errorf("was expecting \"mock request #1 missed because the route matches but there was no response available\", got: %s", body)
	}
}

How is a request selected

In case multiple requests match the incoming one then the first one, by order of registration, matching that still has unconsumed responses will be selected. So for example

package main

import (
	"net/http"
	"testing"

	"github.com/dfioravanti/httpregistry"
)

func TestMultipleMatchingWorks(t *testing.T) {
	// 1. Create the registry and defer the check that all responses
	//    that we will create are used.
	//    t is used to fail the test if the deferred check fails.
	registry := httpregistry.NewRegistry(t)
	defer registry.CheckAllResponsesAreConsumed()

	// 2. Add requests to the registry
	registry.AddMethodAndURLWithStatusCode(
        http.MethodGet, "/users", http.StatusOK,
    )
	registry.AddMethodAndURLWithStatusCode(
        http.MethodGet, "/users", http.StatusNotFound,
    )

	// 3. Create the server
	server := registry.GetServer()
	defer server.Close()

	// 4. Make calls
	response, err := http.Get(server.URL + "/users")
	if err != nil {
		t.Errorf("executing request failed: %v", err)
	}
	if response.StatusCode != 200 {
		t.Errorf("unexpected status code %v", response.StatusCode)
	}

	response, err = http.Get(server.URL + "/users")
	if err != nil {
		t.Errorf("executing request failed: %v", err)
	}
	if response.StatusCode != 404 {
		t.Errorf("unexpected status code %v", response.StatusCode)
	}
}

Documentation

Overview

Package httpregistry provides multiple utilities that can be used to simplify the creation of /net/http/httptest mock servers. That package allows the creation of http servers that can be used to respond to actual http calls in tests. This package aims at providing a nicer interface that should cover the most standard cases and attempts to hide away a layer of boilerplate. For example it is normal to write test code like this

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet && r.URL.Path == "/users" {
		w.WriteHeader(http.StatusOK)
		return
	}

	w.WriteHeader(http.StatusInternalServerError)
}))

with this package this can be simplified to

registry := NewRegistry(t)
registry.AddMethodAndURL("/users", http.MethodGet)
ts := registry.GetServer()

Similarly this package tries to help with the harder task to test if a POST/PUT request actually happen to have the expected body/parameters. With this library this can be done as

registry := NewRegistry(t)
registry.AddRequest(
	httpregistry.Request().
	WithURL("/users").
	WithMethod(http.MethodPost).
	WithJSONHeader().
	WithBody([]byte("{\"user\": \"John Schmidt\"}"))
ts := registry.GetServer()

For more examples of what this package is capable of, refer to the README file.

Index

Constants

This section is empty.

Variables

View Source
var (
	ContinueResponse           = newResponseWithName("httpregistry.ContinueResponse").WithStatus(100)
	SwitchingProtocolsResponse = newResponseWithName("httpregistry.SwitchingProtocolsResponse").WithStatus(101)
	ProcessingResponse         = newResponseWithName("httpregistry.ProcessingResponse").WithStatus(102)
	EarlyHintsResponse         = newResponseWithName("httpregistry.EarlyHintsResponse").WithStatus(103)
)

Information responses

View Source
var (
	OkResponse                          = newResponseWithName("httpregistry.OkResponse").WithStatus(200)
	CreatedResponse                     = newResponseWithName("httpregistry.CreatedResponse").WithStatus(201)
	AcceptedResponse                    = newResponseWithName("httpregistry.AcceptedResponse").WithStatus(202)
	NonAuthoritativeInformationResponse = newResponseWithName("httpregistry.NonAuthoritativeInformationResponse").WithStatus(203)
	NoContentResponse                   = newResponseWithName("httpregistry.NoContentResponse").WithStatus(204)
	ResetContentResponse                = newResponseWithName("httpregistry.ResetContentResponse").WithStatus(205)
	PartialContentResponse              = newResponseWithName("httpregistry.PartialContentResponse").WithStatus(206)
	MultiStatusResponse                 = newResponseWithName("httpregistry.MultiStatusResponse").WithStatus(207)
	AlreadyReportedResponse             = newResponseWithName("httpregistry.AlreadyReportedResponse").WithStatus(208)
)

Successful responses

View Source
var (
	MultipleChoicesResponse   = newResponseWithName("httpregistry.MultipleChoicesResponse").WithStatus(300)
	MovedPermanentlyResponse  = newResponseWithName("httpregistry.MovedPermanentlyResponse").WithStatus(301)
	FoundResponse             = newResponseWithName("httpregistry.FoundResponse").WithStatus(302)
	SeeOtherResponse          = newResponseWithName("httpregistry.SeeOtherResponse").WithStatus(303)
	NotModifiedResponse       = newResponseWithName("httpregistry.NotModifiedResponse").WithStatus(304)
	TemporaryRedirectResponse = newResponseWithName("httpregistry.TemporaryRedirectResponse").WithStatus(307)
	PermanentRedirectResponse = newResponseWithName("httpregistry.PermanentRedirectResponse").WithStatus(308)
)

Redirection messages

View Source
var (
	BadRequestsResponse                 = newResponseWithName("httpregistry.BadRequestsResponse").WithStatus(400)
	UnauthorizedResponse                = newResponseWithName("httpregistry.UnauthorizedResponse").WithStatus(401)
	PaymentRequiredResponse             = newResponseWithName("httpregistry.PaymentRequiredResponse").WithStatus(402)
	ForbiddenResponse                   = newResponseWithName("httpregistry.ForbiddenResponse").WithStatus(403)
	NotFoundResponse                    = newResponseWithName("httpregistry.NotFoundResponse").WithStatus(404)
	MethodNotAllowedResponse            = newResponseWithName("httpregistry.MethodNotAllowedResponse").WithStatus(405)
	NotAcceptableResponse               = newResponseWithName("httpregistry.NotAcceptableResponse").WithStatus(406)
	ProxyAuthenticationRequiredResponse = newResponseWithName("httpregistry.ProxyAuthenticationRequiredResponse").WithStatus(407)
	RequestTimeoutResponse              = newResponseWithName("httpregistry.RequestTimeoutResponse").WithStatus(408)
	ConflictResponse                    = newResponseWithName("httpregistry.ConflictResponse").WithStatus(409)
	GoneResponse                        = newResponseWithName("httpregistry.GoneResponse").WithStatus(410)
	LengthRequiredResponse              = newResponseWithName("httpregistry.LengthRequiredResponse").WithStatus(411)
	PreconditionFailedResponse          = newResponseWithName("httpregistry.PreconditionFailedResponse").WithStatus(412)
	PayloadTooLargeResponse             = newResponseWithName("httpregistry.PayloadTooLargeResponse").WithStatus(413)
	URITooLongResponse                  = newResponseWithName("httpregistry.URITooLongResponse").WithStatus(414)
	UnsupportedMediaTypeResponse        = newResponseWithName("httpregistry.UnsupportedMediaTypeResponse").WithStatus(415)
	RangeNotSatisfiableResponse         = newResponseWithName("httpregistry.RangeNotSatisfiableResponse").WithStatus(416)
	ExpectationFailedResponse           = newResponseWithName("httpregistry.ExpectationFailedResponse").WithStatus(417)
	IAmATeapotResponse                  = newResponseWithName("httpregistry.IAmATeapotResponse").WithStatus(418)
	MisdirectedRequestResponse          = newResponseWithName("httpregistry.MisdirectedRequestResponse").WithStatus(421)
	UpgradeRequiredResponse             = newResponseWithName("httpregistry.UpgradeRequiredResponse").WithStatus(426)
	ReconditionRequiredResponse         = newResponseWithName("httpregistry.ReconditionRequiredResponse").WithStatus(428)
	RequestHeaderFieldsTooLargeResponse = newResponseWithName("httpregistry.RequestHeaderFieldsTooLargeResponse").WithStatus(431)
	UnavailableForLegalReasonsResponse  = newResponseWithName("httpregistry.UnavailableForLegalReasonsResponse").WithStatus(451)
)

Client error responses

View Source
var (
	InternalServerErrorResponse     = newResponseWithName("httpregistry.InternalServerErrorResponse").WithStatus(500)
	NotImplementedResponse          = newResponseWithName("httpregistry.NotImplementedResponse").WithStatus(501)
	BadGatewayResponse              = newResponseWithName("httpregistry.BadGatewayResponse").WithStatus(502)
	ServiceUnavailableResponse      = newResponseWithName("httpregistry.ServiceUnavailableResponse").WithStatus(503)
	GatewayTimeoutResponse          = newResponseWithName("httpregistry.GatewayTimeoutResponse").WithStatus(504)
	HTTPVersionNotSupportedResponse = newResponseWithName("httpregistry.HTTPVersionNotSupportedResponse").WithStatus(505)
	VariantAlsoNegotiatesResponse   = newResponseWithName("httpregistry.VariantAlsoNegotiatesResponse").WithStatus(506)
	NotExtendedResponse             = newResponseWithName("httpregistry.NotExtendedResponse").WithStatus(510)
	NetworkAuthenticationResponse   = newResponseWithName("httpregistry.NetworkAuthenticationResponse").WithStatus(511)
)

Server error responses

View Source
var DefaultRequest = newRequestWithName("httpregistry.DefaultRequest")

DefaultRequest represents the request that is used when no request is specified. It will match any request. This is useful in combination with registry.GetMatchesForRequest so that it is possible to retrieve all the matches associated with it

Functions

This section is empty.

Types

type CustomResponse

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

CustomResponse allows the user to define a custom made response to any request. In particular it allows to define responses that are functions of the request

for example

func(w http.ResponseWriter, r *http.Request) {
	regexUser := regexp.MustCompile(`/users/(?P<userID>.+)/address$`)
	if regexUser.MatchString(r.URL.Path) {
		matches := regexUser.FindStringSubmatch(r.URL.Path)
		userID := matches[regexUser.SubexpIndex("userID")]
		body := map[string]string{"user_id": userID}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(&body)
		return
	}
}

func NewCustomResponse

func NewCustomResponse(f func(w http.ResponseWriter, r *http.Request)) CustomResponse

NewCustomResponse creates a new FunctionalResponse. A FunctionalResponse allows the user to define a custom made response to any request. In particular it allows to define responses that are functions of the request

for example

func(w http.ResponseWriter, r *http.Request) {
	regexUser := regexp.MustCompile(`/users/(?P<userID>.+)/address$`)
	if regexUser.MatchString(r.URL.Path) {
		matches := regexUser.FindStringSubmatch(r.URL.Path)
		userID := matches[regexUser.SubexpIndex("userID")]
		body := map[string]string{"user_id": userID}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(&body)
		return
	}
}

func (CustomResponse) String

func (res CustomResponse) String() string

String marshal CustomResponse to string

func (CustomResponse) WithName

func (res CustomResponse) WithName(name string) CustomResponse

WithName allows to add a name to a FunctionalResponse so that it can be better identified when debugging. By the fault FunctionalResponse gets a sequential name that can be hard to identify if there are many of them

type MockTestingT

type MockTestingT struct {
	HasFailed bool
	Messages  []string
}

MockTestingT mocks the testing.T interface and it can be used to assert that test that should fail will fail

func NewMockTestingT

func NewMockTestingT() *MockTestingT

NewMockTestingT returns a MockTestingT that can be passed as argument of httpregistry.NewRegistry so that is possible to make assertions on the state of the test or on the message that it returns

func (*MockTestingT) Errorf

func (f *MockTestingT) Errorf(format string, args ...any)

Errorf records what error message was emitted

func (*MockTestingT) Fail

func (f *MockTestingT) Fail()

Fail records that the Fail function was called

type Registry

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

Registry represents a collection of matches that associate to a http request a http response. It contains all the Match that were registered and after the server is called it contains all the reasons why a request did not match with a particular match the testing.T is used to signal that there was an unexpected error or that not all the responses were consumed as expected

func NewRegistry

func NewRegistry(t TestingT) *Registry

NewRegistry creates a new empty Registry

func (*Registry) Add

func (reg *Registry) Add()

Add adds to the registry a 200 response for any requests

reg := httpregistry.NewRegistry(t)
reg.Add()
reg.GetServer()

will create a http server that returns 200 on calling anything.

func (*Registry) AddBody

func (reg *Registry) AddBody(body []byte)

AddBody adds to the registry a statusCode response for a request that matches method and URL

reg := httpregistry.NewRegistry(t)
reg.AddSimpleRequest(PUT, "/foo", 204)
reg.GetServer()

will create a http server that returns 204 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddInfiniteResponse

func (reg *Registry) AddInfiniteResponse(response mockResponse)

AddInfiniteResponse adds to the registry a generic response that is returned for any call and it is never consumed

reg := httpregistry.NewRegistry(t)
reg.AddInfiniteResponse(
	httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
)
reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling the server on any URL for as many times as needed

func (*Registry) AddMethod

func (reg *Registry) AddMethod(method string)

AddMethod adds to the registry a 200 response for a request that matches the method

reg := httpregistry.NewRegistry(t)
reg.AddMethod("/foo")
reg.GetServer()

will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddMethodAndURL

func (reg *Registry) AddMethodAndURL(method string, URL string)

AddMethodAndURL adds to the registry a 200 response for a request that matches method and URL

reg := httpregistry.NewRegistry(t)
reg.AddMethodAndURL(GET, "/foo")
reg.GetServer()

will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddMethodAndURLWithStatusCode

func (reg *Registry) AddMethodAndURLWithStatusCode(method string, URL string, statusCode int)

AddMethodAndURLWithStatusCode adds to the registry a statusCode response for a request that matches method and URL

reg := httpregistry.NewRegistry(t)
reg.AddSimpleRequest(PUT, "/foo", 204)
reg.GetServer()

will create a http server that returns 204 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddMethodWithStatusCode

func (reg *Registry) AddMethodWithStatusCode(method string, statusCode int)

AddMethodWithStatusCode adds to the registry a statusCode response for a request that matches the method

reg := httpregistry.NewRegistry(t)
reg.AddMethodWithStatusCode("/foo", 401)
reg.GetServer()

will create a http server that returns 401 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddRequest

func (reg *Registry) AddRequest(request Request)

AddRequest adds to the registry a 200 response for a generic request that needs to be matched

reg := httpregistry.NewRegistry(t)
reg.AddRequest(
	httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value"))
)
reg.GetServer()

will create a http server that returns 200 on calling GET "/foo" with the correct header and fails the test on anything else

func (*Registry) AddRequestWithInfiniteResponse

func (reg *Registry) AddRequestWithInfiniteResponse(request Request, response mockResponse)

AddRequestWithInfiniteResponse adds to the registry a generic response for a generic request that needs to be matched

reg := httpregistry.NewRegistry(t)
reg.AddRequestWithInfiniteResponse(
	httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value")),
	httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
)
reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling GET "/foo" with the correct header and fails the test on anything else

func (*Registry) AddRequestWithResponse

func (reg *Registry) AddRequestWithResponse(request Request, response mockResponse)

AddRequestWithResponse adds to the registry a generic response for a generic request that needs to be matched

reg := httpregistry.NewRegistry(t)
reg.AddRequest(
	httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value")),
	httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
)
reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling GET "/foo" with the correct header and fails the test on anything else

func (*Registry) AddRequestWithResponses

func (reg *Registry) AddRequestWithResponses(request Request, responses ...mockResponse)

AddRequestWithResponses adds to the registry multiple responses for a generic request that needs to be matched. The responses are consumed by the calls so if more calls than responses will happen then the test will fail

reg := httpregistry.NewRegistry(t)
reg.AddRequestWithResponses(
	httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value")),
	httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
	httpregistry.NewResponse(http.Ok, []byte{"hello again"}),
)
reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling GET "/foo" the first call with the correct header, it returns 200 with "hello again" as body on the second call with the correct header and fails the test on anything else

func (*Registry) AddResponse

func (reg *Registry) AddResponse(response mockResponse)

AddResponse adds to the registry a generic response that is returned for any call

reg := httpregistry.NewRegistry(t)
reg.AddResponse(
	httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
)
reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling the server on any URL

func (*Registry) AddResponses

func (reg *Registry) AddResponses(responses ...mockResponse)

AddResponses adds to the registry a generic response that is returned for any call

	reg := httpregistry.NewRegistry(t)
	reg.AddResponses(
		httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
     httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
	)
	reg.GetServer()

will create a http server that returns 204 with "hello" as body on calling the server on any URL for two times and then returns an error

func (*Registry) AddURL

func (reg *Registry) AddURL(URL string)

AddURL adds to the registry a 200 response for a request that matches the URL

reg := httpregistry.NewRegistry(t)
reg.AddURL("/foo")
reg.GetServer()

will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else

func (*Registry) AddURLWithStatusCode

func (reg *Registry) AddURLWithStatusCode(URL string, statusCode int)

AddURLWithStatusCode adds to the registry a statusCode response for a request that matches the URL

reg := httpregistry.NewRegistry(t)
reg.AddURLWithStatusCode("/foo", 401)
reg.GetServer()

will create a http server that returns 401 on calling GET "/foo" and fails the test on anything else

func (*Registry) CheckAllResponsesAreConsumed

func (reg *Registry) CheckAllResponsesAreConsumed()

CheckAllResponsesAreConsumed fails the test if there are unused responses at the end of the test. This is useful to check if all the expected calls happened or if there is an unexpected behavior happening.

**Important**: If you are using AddInfiniteRequest this call will ALWAYS fail!

func (*Registry) GetMatchesForRequest

func (reg *Registry) GetMatchesForRequest(r Request) []*http.Request

GetMatchesForRequest returns the *http.Request that matched a generic Request

func (*Registry) GetMatchesForURL

func (reg *Registry) GetMatchesForURL(url string) []*http.Request

GetMatchesForURL returns the http.Requests that matched a specific URL independently of the method used to call it

func (*Registry) GetMatchesURLAndMethod

func (reg *Registry) GetMatchesURLAndMethod(url string, method string) []*http.Request

GetMatchesURLAndMethod returns the http.Requests that matched a specific method, URL pair

func (*Registry) GetServer

func (reg *Registry) GetServer() *httptest.Server

GetServer returns a httptest.Server designed to match all the requests registered with the Registry

func (*Registry) Why

func (reg *Registry) Why() string

Why returns a string that contains all the reasons why the request submitted to the registry failed to match with the registered requests. The envision use of this function is just as a helper when debugging the tests, most of the time it might not be obvious if there is a typo or a small error.

type Request

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

Request represents a request that will be registered to a Registry to get matched against an incoming HTTP request. The match happens against the method, the headers and the URL interpreted as a regex

func NewRequest

func NewRequest() Request

NewRequest creates a new request designed to be registered to a Registry to get matched against an incoming HTTP request. This function is designed to be used in conjunction with other other receivers. For example

NewRequest().
	WithURL("/users/1").
	WithMethod(http.MethodPatch).
	WithJSONHeader().
	WithBody([]byte("{\"user\": \"John Schmidt\"}"))

func (Request) Equal

func (r Request) Equal(r2 Request) bool

Equal checks if a request is identical to another

func (Request) String

func (r Request) String() string

String returns the name associated with the request

func (Request) WithBody

func (r Request) WithBody(body []byte) Request

WithBody returns a new request with the method body set to body

func (Request) WithHeader

func (r Request) WithHeader(header string, value string) Request

WithHeader returns a new request with the header header set to value

func (Request) WithHeaders

func (r Request) WithHeaders(headers map[string]string) Request

WithHeaders returns a new request with all the headers in headers applied. If multiple headers with the same name are defined only the last one is applied.

func (Request) WithJSONBody

func (r Request) WithJSONBody(body any) Request

WithJSONBody returns a new request with the method body set to the JSON encoded version of body and the Content-Type header set to "application/json". This method panics if body cannot be converted to JSON

func (Request) WithJSONHeader

func (r Request) WithJSONHeader() Request

WithJSONHeader returns a new request with the header `Content-Type` set to `application/json`

func (Request) WithMethod

func (r Request) WithMethod(method string) Request

WithMethod returns a new request with the method attribute set to method

func (Request) WithName

func (r Request) WithName(name string) Request

WithName allows to add a name to a Request so that it can be better identified when debugging. By the default Request gets a sequential name that can be hard to identify if there are many of them. So if clarity is needed we recommend to change the default name.

func (Request) WithStringBody

func (r Request) WithStringBody(body string) Request

WithStringBody returns a new request with the method body set to body

func (Request) WithURL

func (r Request) WithURL(URL string) Request

WithURL returns a new request with the URL attribute set to URL

type Response

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

Response represents a response that we want to return if the registry finds a request that matches the incoming request. If the match happens then we will return a http response that matches the attributes defined in this struct.

func NewResponse

func NewResponse() Response

NewResponse creates a new Response. This function is designed to be used in conjunction with other other receivers. For example

NewResponse().
	WithStatus(http.StatusOK).
	WithJSONHeader().
	WithBody([]byte("{\"user\": \"John Schmidt\"}"))

The default response is a 200 without any body nor header

func (Response) String

func (res Response) String() string

String marshal Response to string

func (Response) WithBody

func (res Response) WithBody(body []byte) Response

WithBody returns a new request with the method body set to body

func (Response) WithHeader

func (res Response) WithHeader(header string, value string) Response

WithHeader returns a new response with the header header set to value

func (Response) WithHeaders

func (res Response) WithHeaders(headers map[string]string) Response

WithHeaders returns a new response with all the headers in headers applied. If multiple headers with the same name are defined only the last one is applied.

func (Response) WithJSONBody

func (res Response) WithJSONBody(body any) Response

WithJSONBody returns a new response that will return body as body and will have the header `Content-Type` set to `application/json`. This method panics if body cannot be converted to JSON

func (Response) WithJSONHeader

func (res Response) WithJSONHeader() Response

WithJSONHeader returns a new Response with the header `Content-Type` set to `application/json`

func (Response) WithName

func (res Response) WithName(name string) Response

WithName allows to add a name to a Response so that it can be better identified when debugging. By the default Response gets a sequential name that can be hard to identify if there are many of them. So if clarity is needed we recommend to change the default name.

func (Response) WithStatus

func (res Response) WithStatus(statusCode int) Response

WithStatus returns a new response with the StatusCode attribute set to statusCode

type TestingT

type TestingT interface {
	Fail()
	Errorf(format string, args ...any)
}

TestingT is the subset of testing.T (see also testing.TB) used by the httpregistry package. The reason why this exists is so that we can mock in test and check if failures happen when we expect. See the readme or the tests for an example of how to use this. By design testing.TB make it impossible for the end user to implement the interface so this is the only way to do so

Jump to

Keyboard shortcuts

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