Hex - Expectations for HTTP handlers

Hex - Http EXpectations

Hex is a simple wrapper that extends httptest.Server with an expectation syntax, allowing you to create mock APIs using a simple and expressive DSL:

func TestUserClient(t*testing.T) {
	// A mock of some remote user service
	server := hex.NewServer(t, nil)	 // nil, or optional http.Handler

	server.ExpectReq("GET", "/users").
		WithHeader("Authorization", "Bearer xxyyzz")
		WithQuery("search", "foo").
		RespondWith(200, `{"id": 123, "name": "test_user"}`)

	// Actual client implementation would go here
	http.Get(server.URL + "/foo")

	// Output:
	// example_test.go:12: One or more HTTP expectations failed
	// example_test.go:12: Expectations
	// example_test.go:12: 	GET /users with header matching Authorization="Bearer xyz"query string matching search="foo" - failed, no matching requests
	// example_test.go:12: Unmatched Requests
	// example_test.go:12: 	GET /foo
}

Getting Started

Hex provides a higher level Server which embeds http.Server. Create one with hex.NewServer, and start making expectations. The arguments to hex.NewServer are a *testing.T, and an optional http.Handler which may be nil. If an existing http.Handler is passed to NewServer, hex will pass requests to it after checking them against its tree of expectations.

s := hex.NewServer(t, http.HandlerFunc(rw ResponseWriter, req *http.Request) {
	fmt.Fprintf(tw, "Ok")
})

s.ExpectReq("GET", "/users").WithQuery("page", "1")

http.Get(s.URL + "/users?page=1") // Match

If you have an existing mock, it can embed an hex.Expecter, which provides ExpectReq for setting up expectations, and LogReq for logging incoming requests so they can be matched against expectations. Server does exactly this, and serves an an example of how to write up the necessary plumbing.

Matching Requests

Expectations are setup via ExpectReq, which accepts an HTTP method (one of "GET", "POST", "PATCH", etc) and a path (not including query string):

server.ExpectReq("GET", "/path/to/resource")

ExpectReq accepts one or two interface{} values, where each value is one of the following:

  • A string, ie "GET" or "/path/to/resource
  • A regular expression created via regexp.MustCompile or the convenience method hex.R
  • A built-in matcher like hex.Any or hex.None
  • A function of type hex.MatchFn (func(req*http.Request) bool)
  • A map[interface{}]interface{} which can recursively contain any of the above (typically only useful for matching against header/body/query string)

Reporting Failure

hex will automatically report failures, and let you know which HTTP requests were made that didn't match any expectations:

func TestExample(t *testing.T) {
	s := hex.NewServer(t, nil)

	s.ExpectReq("GET", "/foo")

	http.Get(s.URL + "/bar")
}

Output:

$ go test ./example
--- FAIL: TestExample (0.00s)
    server.go:29: One or more HTTP expectations failed
    print.go:205: Expectations
    print.go:205: 	GET /foo - failed, no matching requests
    print.go:205: Unmatched Requests
    print.go:205: 	GET /bar
FAIL
FAIL	github.com/meagar/hex/example	0.260s
FAIL

Matching against strings, regular expressions, functions and more

Any key or value given to ExpectReq, WithQuery, WithHeader or WithBody can one of:

  • A string, in which case case-sensitive exact matching is used:

     server.ExpectReq("GET", "/users") // matches GET /users?foo=bar
  • A regular expression (via regexp.MustCompile or hex.R):

     server.ExpectReq(hex.R("^(POST|PATCH)$", hex.R("^users/\d+$")
  • One of several predefined constants like hex.Any or hex.None

     server.ExpectReq("GET", hex.Any)                             // matches any GET request
     server.ExpectReq(hex.Any, hex.Any)                           // matches *any* request
     server.ExpectReq(hex.Any, hex.Any).WithQuery(hex.Any, "123") // Matches any request with any query string parameter having the value "123"
  • A map of interface{}/interface{} pairs, where each interface{} value is itself a string/regex/map/

     server.ExpectReq("GET", "/search").WithQuery(hex.P{
     	"q": "test",
     	"page": hex.R(`^\d+$`),
     })

Matching against the query string, header and body

You can make expectations about the query string, headers or form body with WithQuery, WithHeader and WithBody respectively:

func TestClientLibrary(t*testing.T) {
	t.Run("It includes the authorization header", func(t*testing.T) {
		server := hex.NewServer(t, nil)
		server.ExpectReq("GET", "/users").WithHeader("Authorization", hex.R("^Bearer .+$"))
		// ...
		client.GetUsers()
	})

	t.Run("It includes the user Id in the query string", func(t*testing.T) {
		server := hex.NewServer(t, nil)
		server.ExpectReq("GET", "/users").WithQuery("id", "123")
		// ...
		client.GetUser("123")
	})
}

When only one argument is given to any With* method, matching is done against the key, with any value being accepted:toc:

server.ExpectReq("GET", "/users").WithQuery("id")
// ...
http.Get(server.URL + "/users")              // fail
http.Get(server.URL + "/users?id")           // pass
http.Get(server.URL + "/users?id=1")         // pass
http.Get(server.URL + "/users?id=1&foo=bar") // pass

When no arguments are given, WithQuery, WithHeader and WithBody match any request with a non-empty query/header/body respectively.

server.ExpectReq("GET", "/users").WithQuery()
// ...
http.Get(server.URL + "/users")         // fail
http.Get(server.URL + "/users?foo")     // pass
http.Get(server.URL + "/users?foo=bar") // pass
http.Get(server.URL + "/users?foo=bar") // pass

Mocking Responses

By default, hex will pass requests to the http.Handler object you provide through NewServer (if any). You can override the response with RespondWith(status int, body string), ResponseWidthFn(func(http.ResponseWriter, *http.Request)) or RespondWithHandler(http.Handler):

server := hex.NewServer(t, nil)
server.ExpectReq("GET", "/users").RespondWith("200", "OK")

By default, the http.Handler you provide to NewServer will not be invoked if a requests matches an expectation for which a mock response has been defined. However, you can allow the request to "fall through" and reach your own handler with AndCallThrough. Note that, if your handler writes a response, it will be concatenated to the mock response already produced, and any HTTP status you attempt to write will be silently discarded if a mock response has already set one.:

server := hex.NewServer(t, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
	fmt.Fprintf("BBB")
}))

// Requests matching this expectation will receive a response of "AAABBB"
server.ExpectReq("GET", "/foo").RespondWith(200, "AAA").AndCallThrough()

Scoping with Do

By default, a request issued at any point in a test after an ExpectReq expectation is made will match that expectation.

To limit the scope in which an expectation can be matched, use Do:

server := hex.NewServer(t, nil)
server.ExpectReq("GET", "/users").Do(func() {
	// This will match:
	http.Get(server.URL + "/users")
})
// This will fail, the previous expectation's scope has closed
http.Get(server.URL + "/users")

Once, Never

If a request should only happen once (or not at all) in a given block of code, you can express this expectation with Once or Never:

func TestCaching(t*testing.T) {
	t.Run("The client caches the server's response", func(t*testing.t) {
		server := hex.NewServer(t, nil)
		server.ExpectReq("GET", "/countries").Once()
		// ...
		client.GetCountries()
		client.GetCountries()
		// Output:
		// Expectations
		// 	GET /countries - failed, expected 1 matches, got 2
	})

	t.Run("The client should not make a request if the arguments are invalid", func(t*testing.T) {
		server := hex.NewServer(t, nil)
		server.ExpectReq("GET", "/users").Never()
		// ...
		// Assume the client is not supposed to make requests unless the ID is an integer
		_, err := client.GetUser("foo")
		// assert that err is not nil
	})
})

Helpers R and P

hex.R is a wrapper around regexp.MustCompile, and hex.P ("params") is an alias for map[string]interface{}.

These helpers allow for more succinct definition of matchers:

server := hex.NewServer(t, nil)
server.ExpectReq("GET", hex.R(`/users/\d+`)) // Matches /users/123
// ... 
server.ExpectReq("POST", "/users").WithBody(hex.P{
	"name": hex.R(`^[a-z]+$`),
	"age": hex.R(`^\d+$`),
})

TODO

  • Better support for matching JSON requests
  • Higher level helpers
    • WithBearer
    • WithJsonResponse
    • WithType("json"|"html")
  • hex.Verbose() and ExpectReq(...).Verbose() for debugging
Similar Resources

Mahi is an all-in-one HTTP service for file uploading, processing, serving, and storage.

Mahi is an all-in-one HTTP service for file uploading, processing, serving, and storage.

Mahi is an all-in-one HTTP service for file uploading, processing, serving, and storage. Mahi supports chunked, resumable, and concurrent uploads. Mahi uses Libvips behind the scenes making it extremely fast and memory efficient.

Dec 29, 2022

HTTP/2 Apple Push Notification service (APNs) provider for Go with token-based connection

APNs Provider HTTP/2 Apple Push Notification service (APNs) provider for Go with token-based connection Example: key, err := apns.AuthKeyFromFile("Aut

Dec 29, 2022

OpenID Connect (OIDC) http middleware for Go

Go OpenID Connect (OIDC) HTTP Middleware Introduction This is a middleware for http to make it easy to use OpenID Connect. Currently Supported framewo

Jan 1, 2023

Statigz serves pre-compressed embedded files with http in Go

statigz statigz serves pre-compressed embedded files with http in Go 1.16 and later. Why? Since version 1.16 Go provides standard way to embed static

Dec 24, 2022

A Concurrent HTTP Static file server using golang .

A Concurrent HTTP static server using Golang. Serve Static files like HTML,CSS,Js,Images,Videos ,ect. using HTTP. It is Concurrent and Highly Scalable.Try now!

Dec 19, 2021

Go HTTP middleware to filter clients by IP

Go HTTP middleware to filter clients by IP

Oct 30, 2022

A dead simple, stupid, http service.

A dead simple, stupid, http service implemented in a complicated way just for the sake of following Go design patterns and scalability. Useful for learning and testing basic kubernetes networking. Made on an insomniac night.

Sep 2, 2022

A http service to verify request and bounce them according to decisions made by CrowdSec.

traefik-crowdsec-bouncer A http service to verify request and bounce them according to decisions made by CrowdSec. Description This repository aim to

Dec 21, 2022

Chi ip banner is a chi middleware that bans some ips from your Chi http server.

Chi Ip Banner Chi ip banner is a chi middleware that bans some ips from your Chi http server. It reads a .txt file in your project's root, called bani

Jan 4, 2022
Related tags
echo-http - Echo http service

echo-http - Echo http service Responds with json-formatted echo of the incoming request and with a predefined message. Can be install directly (go get

Dec 4, 2022
Composable chains of nested http.Handler instances.

chain go get github.com/codemodus/chain Package chain aids the composition of nested http.Handler instances. Nesting functions is a simple concept. I

Sep 27, 2022
Go http.Hander based middleware stack with context sharing

wrap Package wrap creates a fast and flexible middleware stack for http.Handlers. Features small; core is only 13 LOC based on http.Handler interface;

Apr 5, 2022
Minimalist net/http middleware for golang

interpose Interpose is a minimalist net/http middleware framework for golang. It uses http.Handler as its core unit of functionality, minimizing compl

Sep 27, 2022
Add interceptors to GO http.Client

mediary Add interceptors to http.Client and you will be able to Dump request and/or response to a Log Alter your requests before they are sent or resp

Nov 17, 2022
Lightweight Middleware for net/http

MuxChain MuxChain is a small package designed to complement net/http for specifying chains of handlers. With it, you can succinctly compose layers of

Dec 10, 2022
Idiomatic HTTP Middleware for Golang

Negroni Notice: This is the library formerly known as github.com/codegangsta/negroni -- Github will automatically redirect requests to this repository

Jan 2, 2023
A collection of useful middleware for Go HTTP services & web applications 🛃

gorilla/handlers Package handlers is a collection of handlers (aka "HTTP middleware") for use with Go's net/http package (or any framework supporting

Dec 31, 2022
Simple middleware to rate-limit HTTP requests.

Tollbooth This is a generic middleware to rate-limit HTTP requests. NOTE 1: This library is considered finished. NOTE 2: Major version changes are bac

Dec 28, 2022
A HTTP mole service
A HTTP mole service

httpmole provides a HTTP mock server that will act as a mole among your services, telling you everything http clients send to it and responding them whatever you want it to respond. Just like an actual mole.

Jul 27, 2022