httpmockserver

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 9, 2023 License: MIT Imports: 17 Imported by: 0

README

Go Report Card Go build Codecov GoDoc GitHub license

HTTP Mock Server for golang

HTTP Mock Server provides an easy way to mock external http resources instead of mocking the whole application.

Supports:

  • expectations of resources that should be requested during the test
    • request method (GET, POST, PUT, DELETE, ...)
    • request path (e.g. /api/v1/users)
    • request forms / query parameters
    • request headers
    • request auth headers (e.g. basic auth, jwt token)
    • request body
    • custom expectations (if none of the above is sufficient)
  • response mocking
    • response status code
    • response headers
    • response body
  • verification of expectations
    • verify that all expectations were met
    • verify that no unexpected requests were made
  • default expectations as catch all (if no other expectation matches)
  • "every" expectations, that matches on every request (e.g. Content-Type header for all must be application/json)
  • starts a http server on a random port (you can also specify a port)
  • supports http and https
  • integrates into t *testing.T and returns helpful error messages

Installation

go get -u github.com/ybbus/httpmockserver

Getting started

To start simple, let's assume our application uses an external http service to retrieve user information. We want to test our application, but we don't want to call the external service during the test. We also don't want to mock the http client since we want to be sure that the resulting http request is correct.

The application retrieves a user information from the external service: GET /api/v1/users

So in our tests we want to expect a GET request to /api/v1/users and return a mocked response.

This can be easily done with httpmockserver:

package main

import (
	"testing"
	"github.com/ybbus/httpmockserver"
)

func Test_Application(t *testing.T) {
	// create a new mock server
	server := httpmockserver.New(t)
	defer server.Shutdown()

	// set expectation(s)
	server.EXPECT().
		Get("/api/v1/users").
		Response(200).
		StringBody(`[{"id": 1, "name": "John Doe"}]`)

	// test application and use the base url of the mock server to initialize the application client
	baseUrl := server.BaseURL() // default: http://127.0.0.1:<random_port>

	server.AssertExpectations()
}

You will now have a http server running on a random port. The server returns the mocked response when GET /api/v1/users is called. If the call was missing, the test will fail with a helpful error message.

Note: The New call expects a *testing.T as parameter. This is required to integrate into the testing framework. Don't be confused that it does not actually require a *testing.T, since it only implements the required methods. This was done for better unit testing (mocking *testing.T), since the library depends heavily on *testing.T.

If you want to use the server without *testing.T, at all, you may just provide your own implementation of T interface.

type T interface {
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Errorf(format string, args ...interface{})
}

In detail

Note: Most of the examples just show the method calls, but you can also chain them together. The examples don't make much sense without chaining them together but it makes them easier to read.

Also some combinations are quite useless like: server.EXPECT().GET().POST(). A request cannot be both a GET and a POST request at the same time.

EVERY() expectations

EVERY() expectations are matched on every request. Normally you only need them if you expect a lot of calls and don't want to specify the same expectation for every call.

EVERY() for request expectations

A valid example would be to expect that all requests have the header "Content-Type: application/json" set:

// expect all incoming requests to have the header "Content-Type: application/json" set
server.EVERY().Header("Content-Type", "application/json")

Or if you only expect GET requests to be called:

// expect all incoming requests to be GET requests
server.EVERY().GET()

Note: EVERY() cannot set Responses, since it matches on every request and that would not make sense.

DEFAULT() matcher

If you want to catch requests that do not match any expectation, you can use DEFAULT() as fallback.

The request is checked against the DEFAULT() expectations, only if all intended expectations do not match.

For example, if you would like to return 404 on all requests that do not match any expectation:

// return 404 on all GET requests that do not match any expectation
server.DEFAULT().Response(404).StringBody(`{"error": "not found"}`)

This will also prevent the test to fail on additional requests that do not match any expectation.

EXPECT() matcher

Use EXPECT() to set the actual expectations of the mock server.

First you define the validators that are used to match the incoming request. Then you define the response that should be returned.

There are a lot of helper methods to set the validators.

Note: Each EXPECT() call must at least contain one validator (e.g. Path("/api/v1/users")). Otherwise the test will fail.

Number of times an expectation should be met

You can specify how often an expectation should be met.

Note: The default is Once() which means that the expectation should be met exactly once.

This can be done by just using the following methods:

MinTimes(2) // should at least be called 2 times
MaxTimes(4) // should at most be called 2 times

There are some shortcuts for the most common cases:

AnyTimes() // should be called any number of times
Once() // should be called exactly once
Twice() // should be called exactly twice
AtMostOnce() // should be called at most once
AtLeastOnce() // should be called at least once
Times(3) // should be called exactly 3 times
Request method and path

The following validation helpers are available for matching the request method and path:

// expect a method without specifying a path
GET()
POST()
PUT()
DELETE()

// expect an exact path to match without specifying a method
Path("/api/v1/users")

// expect a method and an exact path
Get("/api/v1/users")
Post("/api/v1/users")

// expect a custom method and a path
Request("TRACE", "/api/v1/users")
or
Method("TRACE").Path("/api/v1/users")

For the path you may also use a regular expression:

GetMatches(`^/abc/\d+$`) // to match /abc/123 etc.
PathMatches(`^/abc/\d+$`)
RequestMatches("POST", `^/abc/\d+$`)

Note:

  • if no method expectation is set, the expectation will match on every method
  • if no path expectation is set, the expectation will match on every path
Request headers

To validate if the request has a specific header set, you can use the following helpers:

Header("Content-Type", "application/json") // to match the exact header value
HeaderMatches("Content-Type", `^application/(json|xml)$`) // to match application/json or application/xml
HeaderExists("Content-Type") // to check if the header exists

Headers(map[string]string{"Content-Type": "application/json", "Accept": "application/json"}) // to check multiple headers
//same as
Header("Content-Type", "application/json")
Header("Accept", "application/json")

Note: There may be additional headers in the request that are not specified in the expectation. This won't cause the test to fail.

Request query / form parameters
QueryParameter("page", "1")
QueryParameterMatches("page", `^\d+$`)
QueryParameterExists("page")
QueryParameters(map[string]string{"page": "1", "limit": "10"})

FormParameter("client_id", "abc")
FormParameterMatches("client_id", `user_.*`)
FormParameterExists("client_id")
FormParameters(map[string]string{"client_id": "user", "client_secret": "secret"})

Note: There may be additional query parameters in the request that are not specified in the expectation. This won't cause the test to fail.

Authentication

You may want to check authentication headers. The following helpers are available:

// Basic authentication
BasicAuth("alice", "secret") // to match the exact username and password
BasicAuthExists() // to check if Authorization header exists

// JWT token (bearer)
JWTTokenExists() // check if Authorization header with Bearer exists

// JWT Token has a specific claim using json path (see: see: https://github.com/oliveagle/jsonpath) 
JWTTokenClaimPath("$.name", "Jack") // check if token has a claim "name" the value "Jack"
Request body validators

The following validators are available to check the request body:

Body([]byte("Hello World")) // to match the exact body "Hello World"
StringBody("Hello World") // same as Body([]byte("Hello World")), let you provide a string instead of a byte array
StringBodyContains("Hello") // to check if the body contains the string "Hello"
StringBodyMatches(`^Hello.*$`) // to check if the body matches the regular expression
JSONBody(object interface{}) // to check if the body is a valid json and matches the given object
JSONPathContains("$.name", "Jack") // to check if the json body contains the given json path (see: https://github.com/oliveagle/jsonpath)

BodyFunc(func(body []byte) error {
	// check if the body matches your custom logic
    return nil // or return an error if the body does not match
})

Note:

JSONBody expects a given request with a specific body. The body can be either be a go object that wil be parsed to a json string (e.g. map[string]string{"foo":"bar"}) or a json string (e.g. {"foo":"bar"}). The body will be normalized (e.g. whitespace will be removed, fields will be sorted) and compared with the body by string equality.

Response()

When you are done with the expectations, you can set the response that should be returned when the expectation is met.

The following methods are available to set the response:

Note: To switch from the expectation to the response, you must call Response(int) as first call.

Response(200) // to set the status code
Header("Content-Type", "application/json") // to set a response header
Headers(map[string]string{"Content-Type": "application/json", "Accept": "application/json"}) // to set multiple response headers
StringBody("Hello World") // to set the response body as string
Body([]byte("Hello World")) // same as StringBody("Hello World"), let you provide a byte array instead of a string
JsonBody(object interface{}) // to set the response body as json (may provide a go object or a string that is valid json)

Example:

server.EXPECT().
	  Post("/api/v1/users").
	  Header("Content-Type", "application/json").
	  JSONPathContains("$.name", "Jack").
	  Times(2).
	Response(201).
	  Header("Content-Type", "application/json").
	  StringBody(`{"id": 123, "name": "Jack"}`)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type IncomingRequest

type IncomingRequest struct {
	R    *http.Request
	Body []byte
}

type LoggerT added in v1.0.5

type LoggerT struct {
}

func (*LoggerT) Errorf added in v1.0.5

func (l *LoggerT) Errorf(format string, args ...interface{})

func (*LoggerT) Fatal added in v1.0.5

func (l *LoggerT) Fatal(args ...interface{})

func (*LoggerT) Fatalf added in v1.0.5

func (l *LoggerT) Fatalf(format string, args ...interface{})

func (*LoggerT) Helper added in v1.0.5

func (l *LoggerT) Helper()

type MockResponse

type MockResponse struct {
	Code    int
	Headers map[string]string
	Body    []byte
}

type MockServer

type MockServer interface {
	// BaseURL returns the base url of the mock server (default: http://127.0.0.1:<random_port>)
	BaseURL() string
	// ServeHTTP provides direct access to the http handler, normally this is not required
	ServeHTTP(w http.ResponseWriter, r *http.Request)
	// EVERY returns a RequestExpectation that will match on any call
	// (e.g. all requests should have a specific header, or all requests use GET)
	EVERY() RequestExpectation
	// EXPECT returns a RequestExpectation that can be used to create expectations
	// the default number of calls is expected to be exactly one
	// this can be changed by calling a method like: Times, MinTimes, MaxTimes, etc.
	EXPECT() RequestExpectation
	// DEFAULT returns a RequestExpectation that will be executed if no other expectation matches
	DEFAULT() RequestExpectation
	// AssertExpectations should be called to check if all expectations have been met
	// It also removes all expectations (except the default and every expectations).
	// This let you reuse the same mock server for multiple tests.
	AssertExpectations()
	// Shutdown should be called to stop the mock server (should be deferred at the beginning of the test function)
	Shutdown()
}

func New

func New(t T) MockServer

New creates a new mock server running on http://127.0.0.1:<random_port>

func NewWithOpts added in v0.2.0

func NewWithOpts(t T, opts Opts) MockServer

NewWithOpts can be used to create a mock server with custom options

type Opts added in v0.2.0

type Opts struct {
	// Port is the port the mock server will listen on (default: random port)
	Port string
	// UseSSL is used to enable SSL (default: false)
	UseSSL bool
	// Cert is the certificate used for SSL
	Cert io.Reader
	// Key is the key used for SSL
	Key io.Reader
}

Opts is used to configure the mock server it has reasonable defaults

type RequestExpectation

type RequestExpectation interface {
	// AnyTimes expects a given request any number of times (same as MinTimes(0).MaxTimes(∞))
	AnyTimes() RequestExpectation
	// Once expects a given request exactly once (same as Times(1))
	Once() RequestExpectation
	// Twice expects a given request exactly twice (same as Times(2))
	Twice() RequestExpectation
	// AtMostOnce expects a given request at most once (same as MinTimes(0).MaxTimes(1))
	AtMostOnce() RequestExpectation
	// AtLeastOnce expects a given request at least once (same as MinTimes(1).MaxTimes(∞))
	AtLeastOnce() RequestExpectation
	// Times expects a given request exactly n times (same as MinTimes(n).MaxTimes(n))
	Times(n int) RequestExpectation
	// MinTimes expects a given request at least n times (increases MaxTimes to at least n)
	MinTimes(n int) RequestExpectation
	// MaxTimes expects a given request at most n times (decreases MinTimes to at least n)
	MaxTimes(n int) RequestExpectation

	// Request expects a given request with a specific method and path
	Request(method string, path string) RequestExpectation
	// RequestMatches expects a given request with a specific method and path matching a regex (e.g. `^/foo/bar/\d+$`)
	RequestMatches(method string, pathRegex string) RequestExpectation
	// Method expects a given request with a specific method (e.g. GET, POST, PUT, DELETE)
	Method(method string) RequestExpectation
	// Path expects a given request with a specific path (e.g. /foo/bar)
	Path(path string) RequestExpectation
	// PathMatches expects a given request with a path matching a regex (e.g. `^/foo/bar/\d+$`)
	PathMatches(pathRegex string) RequestExpectation

	// GET expects a given request with a GET method
	// use if no path should be matched (otherwise use Get(path))
	GET() RequestExpectation
	// POST expects a given request with a POST method
	// use if no path should be matched (otherwise use Post(path))
	POST() RequestExpectation
	// PUT expects a given request with a PUT method
	// use if no path should be matched (otherwise use Put(path))
	PUT() RequestExpectation
	// PATCH expects a given request with a PATCH method
	// use if no path should be matched (otherwise use Patch(path))
	PATCH() RequestExpectation
	// DELETE expects a given request with a DELETE method
	// use if no path should be matched (otherwise use Delete(path))
	DELETE() RequestExpectation
	// HEAD expects a given request with a HEAD method
	// use if no path should be matched (otherwise use Head(path))
	HEAD() RequestExpectation

	// Get expects a given request with a GET method and a specific path (e.g. /foo/bar)
	Get(path string) RequestExpectation
	// GetMatches expects a given request with a GET method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	GetMatches(pathRegex string) RequestExpectation
	// Post expects a given request with a POST method and a specific path (e.g. /foo/bar)
	Post(path string) RequestExpectation
	// PostMatches expects a given request with a POST method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	PostMatches(pathRegex string) RequestExpectation
	// Put expects a given request with a PUT method and a specific path (e.g. /foo/bar)
	Put(path string) RequestExpectation
	// PutMatches expects a given request with a PUT method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	PutMatches(pathRegex string) RequestExpectation
	// Patch expects a given request with a PATCH method and a specific path (e.g. /foo/bar)
	Patch(path string) RequestExpectation
	// PatchMatches expects a given request with a PATCH method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	PatchMatches(pathRegex string) RequestExpectation
	// Delete expects a given request with a DELETE method and a specific path (e.g. /foo/bar)
	Delete(path string) RequestExpectation
	// DeleteMatches expects a given request with a DELETE method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	DeleteMatches(pathRegex string) RequestExpectation
	// Head expects a given request with a HEAD method and a specific path (e.g. /foo/bar)
	Head(path string) RequestExpectation
	// HeadMatches expects a given request with a HEAD method and a path matching a regex (e.g. `^/foo/bar/\d+$`)
	HeadMatches(pathRegex string) RequestExpectation

	// Header expects a given request with a specific header (e.g. "Content-Type", "application/json")
	Header(name, value string) RequestExpectation
	// HeaderMatches expects a given request with a header matching a regex (e.g. "Content-Type", `^application/(json|xml)$`)
	HeaderMatches(name, valueRegex string) RequestExpectation
	// HeaderExists expects a given request with a specific header (e.g. "Authorization")
	HeaderExists(name string) RequestExpectation
	// Headers expects a given request with specific list of headers
	Headers(map[string]string) RequestExpectation

	// FormParameter expects a given request with a specific form parameter (e.g. "foo", "bar")
	FormParameter(name, value string) RequestExpectation
	// FormParameterMatches expects a given request with a form parameter matching a regex (e.g. "foo", `^bar\d+$`)
	FormParameterMatches(name string, regex string) RequestExpectation
	// FormParameterExists expects a given request with a specific form parameter (e.g. "foo")
	FormParameterExists(name string) RequestExpectation
	// FormParameters expects a given request with specific list of form parameters
	FormParameters(map[string]string) RequestExpectation

	// QueryParameter expects a given request with a specific query parameter (e.g. "?foo=bar")
	QueryParameter(name, value string) RequestExpectation
	// QueryParameterMatches expects a given request with a query parameter matching a regex (e.g. "?foo=`^bar\d+$`)
	QueryParameterMatches(name string, regex string) RequestExpectation
	// QueryParameterExists expects a given request with a specific query parameter (e.g. "foo")
	QueryParameterExists(name string) RequestExpectation
	// QueryParameters expects a given request with specific list of query parameters
	QueryParameters(map[string]string) RequestExpectation

	// BasicAuth expects a given request with a specific basic auth username and password
	BasicAuth(user, password string) RequestExpectation
	// BasicAuthExists expects a given request with basic auth
	BasicAuthExists() RequestExpectation

	// JWTTokenExists expects a given request with a jwt auth token
	JWTTokenExists() RequestExpectation
	// JWTTokenClaimPath expects a given request with a jwt auth token containing a specific claim using jsonPath notation
	// (e.g. `$.foo.bar` for `{"foo":{"bar":"baz"}}`)
	// see: https://github.com/oliveagle/jsonpath
	JWTTokenClaimPath(jsonPath string, value interface{}) RequestExpectation

	// Body expects a given request with a specific body in bytes (e.g. []byte(`{"foo":"bar"}`))
	Body(body []byte) RequestExpectation
	// StringBody expects a given request with a specific body as string (e.g. `{"foo":"bar"}`)
	StringBody(body string) RequestExpectation
	// StringBodyContains expects a given request with a body containing a specific substring (e.g. `foo`)
	StringBodyContains(substring string) RequestExpectation
	// StringBodyMatches expects a given request with a body matching a regex (e.g. `^abcd\d+$`)
	StringBodyMatches(regex string) RequestExpectation
	// JSONBody expects a given request with a specific body.
	// The body can be either a go object that wil be parsed to a json string (e.g. `map[string]string{"foo":"bar"}`)
	// or a json string (e.g. `{"foo":"bar"}`).
	// The body will be normalized (e.g. whitespace will be removed, fields will be sorted) and compared by string equality.
	JSONBody(object interface{}) RequestExpectation
	// JSONPathContains expects a given request with a body containing a specific json value using jsonPath notation
	// see: https://github.com/oliveagle/jsonpath
	JSONPathContains(jsonPath string, value interface{}) RequestExpectation
	// JSONPathMatches expects a given request with a body matching a regex of a value retrieved by jsonPath notation
	// see: https://github.com/oliveagle/jsonpath
	JSONPathMatches(jsonPath string, regex string) RequestExpectation

	// BodyFunc expects a given request with a custom validation function
	// you can use the provided body to do arbitrary validation
	// return nil if the request matched the given requirements
	// if an error is returned, another expectation is tried (or the default expectation is used, if any)
	BodyFunc(func(body []byte) error) RequestExpectation

	// Custom expects a given request with a custom validation function
	// return nil if the request matched the given requirements
	// if an error is returned, another expectation is tried (or the default expectation is used, if any)
	Custom(validation RequestValidationFunc, description string) RequestExpectation

	// Response returns the given status code and switches to response expectation mode
	// where you can specify the response body and headers
	Response(code int) ResponseExpectation
}

RequestExpectation is used to set expectations on incoming requests EVERY(): used to set expectations that are checked on every request EXPECT(): used to set expectations that are checked on a specific request (specified number of times) DEFAULT(): used to set expectations that are checked on a request if no other expectation matches

type RequestValidationFunc

type RequestValidationFunc func(r *IncomingRequest) error

type ResponseExpectation

type ResponseExpectation interface {
	ContentType(contentType string) ResponseExpectation
	Header(key, value string) ResponseExpectation
	Headers(headers map[string]string) ResponseExpectation
	StringBody(body string) ResponseExpectation
	JsonBody(object interface{}) ResponseExpectation
	Body(data []byte) ResponseExpectation
}

ResponseExpectation is a builder for a MockResponse you may set Headers, Body, and Code on the response this response is returned to the caller when the corresponding request is matched

type T added in v1.0.0

type T interface {
	Helper()
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Errorf(format string, args ...interface{})
}

Jump to

Keyboard shortcuts

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