Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

github.com/meagar/hex

Package Overview
Dependencies
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

github.com/meagar/hex

  • v0.0.5
  • Source
  • Go
  • Socket score

Version published
Created
Source

= HEx - Http EXpectations :toc:

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:

[source, go]

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.

[source,go]

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. https://github.com/meagar/hex/blob/main/server.go([`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):

[source,go]

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:

[source,go]

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

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

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

}

Output:

[source,plain]

$ 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:

[source,go] server.ExpectReq("GET", "/users") // matches GET /users?foo=bar

  • A regular expression (via regexp.MustCompile or hex.R):

[source,go] server.ExpectReq(hex.R("^(POST|PATCH)$", hex.R("^users/\d+$")

  • One of several predefined constants like hex.Any or hex.None

[source,go]

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/

[source,go]

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:

[source,go]

func TestClientLibrary(ttesting.T) { t.Run("It includes the authorization header", func(ttesting.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:

[source,go]

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.

[source,go]

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):

[source,go]

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

[source,go]

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:

[source,go]

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:

[source,go]

func TestCaching(ttesting.T) { t.Run("The client caches the server's response", func(ttesting.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:

[source,go]

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")

FAQs

Package last updated on 02 Aug 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc