Documentation
¶
Overview ¶
Package hex defines an Expecter class for making expect-style assertions about HTTP calls in your test suite.
Its intended use in in mock services, typically those that power httptest.Server instances.
Example ¶
package main
import (
"fmt"
"net/http"
"net/url"
"testing"
"github.com/meagar/hex"
)
// MockUserService is a mock we dependency-inject into our Client library
// It embeds a hex.Server so we can make HTTP requests of it, and use ExpectReq to set up
// HTTP expectations.
type MockUserService struct {
*hex.Server
}
func NewMockUserService(t *testing.T) *MockUserService {
s := MockUserService{}
s.Server = hex.NewServer(t, &s)
return &s
}
func (m *MockUserService) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
path := req.Method + " " + req.URL.Path
switch path {
case "GET /users":
// TODO: Generate the response a client would expect
case "POST /users":
// TODO: Generate the response a client would expect
default:
rw.WriteHeader(http.StatusNotFound)
fmt.Fprintf(rw, "Not found")
}
}
// SearchClient is our real HTTP client for the given service
type UsersClient struct {
Host string
Client *http.Client
}
type User struct {
Name string
Email string
}
// Search hits the "search" endpoint for the given host, with an id query string parameter
func (c *UsersClient) Find(userID int) (User, error) {
_, err := c.Client.Get(fmt.Sprintf("%s/users/%d", c.Host, userID))
// TODO: Decode mock service response
return User{}, err
}
func (c *UsersClient) Create(u User) error {
data := url.Values{}
data.Set("name", u.Name)
data.Set("email", u.Email)
_, err := c.Client.PostForm(c.Host+"/users", data)
return err
}
func main() {
t := testing.T{}
service := NewMockUserService(&t)
// Client is our real client implementation
client := UsersClient{
Client: service.Client(),
Host: service.URL,
}
// Make expectations about the client library
service.ExpectReq("GET", "/users/123").Once().Do(func() {
client.Find(123)
})
service.ExpectReq("POST", "/users").WithBody("name", "User McUser").WithBody("email", "[email protected]").Do(func() {
client.Create(User{
Name: "User McUser",
Email: "[email protected]",
})
})
fmt.Println(service.Summary())
}
Output: Expectations GET /users/123 - passed POST /users with body matching name="User McUser"body matching email="[email protected]" - passed
Index ¶
- func R(pattern string) *regexp.Regexp
- type Expectation
- func (e *Expectation) AndCallThrough() *Expectation
- func (e *Expectation) Do(fn func())
- func (e *Expectation) Never() *Expectation
- func (e *Expectation) Once() *Expectation
- func (e *Expectation) RespondWith(status int, body string) *Expectation
- func (e *Expectation) RespondWithFn(fn func(http.ResponseWriter, *http.Request)) *Expectation
- func (e *Expectation) RespondWithHandler(handler http.Handler) *Expectation
- func (e *Expectation) String() string
- func (e *Expectation) With(fn func(req *http.Request) bool)
- func (e *Expectation) WithBody(args ...interface{}) *Expectation
- func (exp *Expectation) WithHeader(args ...interface{}) *Expectation
- func (exp *Expectation) WithQuery(args ...interface{}) *Expectation
- type Expecter
- func (e *Expecter) ExpectReq(method, path interface{}) (exp *Expectation)
- func (e *Expecter) Fail() bool
- func (e *Expecter) FailedExpectations() (failed []*Expectation)
- func (e *Expecter) HexReport(t TestingT)
- func (e *Expecter) LogReq(req *http.Request) *Expectation
- func (e *Expecter) Pass() bool
- func (e *Expecter) PassedExpectations() (passed []*Expectation)
- func (e *Expecter) Summary() string
- func (e *Expecter) UnmatchedRequests() []*http.Request
- type MatchConst
- type P
- type Server
- type StringMatcher
- type TestingT
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Expectation ¶
type Expectation struct {
// contains filtered or unexported fields
}
Expectation captures details about an ExpectReq call and subsequent conditions chained to it.
func (*Expectation) AndCallThrough ¶
func (e *Expectation) AndCallThrough() *Expectation
AndCallThrough instructs the expectation to run both a registered mock response handler, and then additionally run the original handler
func (*Expectation) Do ¶
func (e *Expectation) Do(fn func())
Do opens a scope. Expectations in the current scope may be matched by requests in the current or nested scopes, but requests in higher scopes cannot fulfill expections in lower scopes.
For example:
expector.ExpectReq("POST", "/foo")
expector.ExpectReq("GET", "/bar").Do(func() {
// matches POST expectation in parent scope
expector.LogReq(httptest.NewRequest("GET" "/foo", nil))
})
// Does NOT match GET expectation in previous scope
expector.LogReq(httptest.NewRequest("GET" "/foo", nil)) // does not match
The current expectation becomes the first expectation within the new scope
func (*Expectation) Never ¶
func (e *Expectation) Never() *Expectation
Never asserts that the expectation is matched zero times
Example ¶
e := Expecter{}
e.ExpectReq("GET", "/users").Never()
e.LogReq(httptest.NewRequest("GET", "/users", nil))
fmt.Println(e.Summary())
Output: Expectations GET /users - failed, expected 0 matches, got 1
func (*Expectation) Once ¶
func (e *Expectation) Once() *Expectation
Once adds a quantity condition that requires exactly one request to be matched
Example ¶
e := Expecter{}
e.ExpectReq("GET", "/status").Once()
e.LogReq(httptest.NewRequest("GET", "/status", nil))
e.LogReq(httptest.NewRequest("GET", "/status", nil))
fmt.Println(e.Summary())
Output: Expectations GET /status - failed, expected 1 matches, got 2
func (*Expectation) RespondWith ¶
func (e *Expectation) RespondWith(status int, body string) *Expectation
RespondWith accepts a status code and string respond body
Example ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"testing"
"github.com/meagar/hex"
)
func main() {
server := hex.NewServer(&testing.T{}, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// The default behavior of the mock service
fmt.Fprintf(rw, "ok")
}))
// Override handler for any requests that match this expectation
server.ExpectReq("GET", "/foo").Once().RespondWith(200, "mock response")
if resp, err := http.Get(server.URL + "/foo"); err != nil {
panic(err)
} else {
// Verify the mock response was received
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if string(body) != "mock response" {
log.Panicf(`Expected body to match "mock response", got %s (%v)`, body, err)
}
}
// Should hit the server's default handler
server.ExpectReq("GET", "/bar")
if resp, err := http.Get(server.URL + "/bar"); err != nil {
log.Panicf("Unexpected error contacting mock service: %v", err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if string(body) != "ok" {
log.Panicf(`Expected body to match "ok", got %s (%v)`, body, err)
}
}
fmt.Println(server.Summary())
}
Output: Expectations GET /foo - passed GET /bar - passed
func (*Expectation) RespondWithFn ¶
func (e *Expectation) RespondWithFn(fn func(http.ResponseWriter, *http.Request)) *Expectation
RespondWithFn adds a mock response using a function that can be passed to http.HandlerFunc
func (*Expectation) RespondWithHandler ¶
func (e *Expectation) RespondWithHandler(handler http.Handler) *Expectation
RespondWithHandler registers an alternate handler to use when the expectation matches a request. Use AndCallThrough to additionally run the original handler, after the new handler is called
func (*Expectation) String ¶
func (e *Expectation) String() string
func (*Expectation) With ¶
func (e *Expectation) With(fn func(req *http.Request) bool)
With adds a generic condition callback that must return true if the request matched, and false otherwise
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"github.com/meagar/hex"
)
func main() {
e := hex.Expecter{}
// With allows custom matching through a callback function
e.ExpectReq("POST", "/users").With(func(req *http.Request) bool {
if err := req.ParseForm(); err != nil {
panic(err)
}
return req.Form.Get("user_id") == "123"
})
body := strings.NewReader(url.Values{
"user_id": []string{"123"},
}.Encode())
req := httptest.NewRequest("POST", "/users", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
e.LogReq(req)
fmt.Println(e.Summary())
}
Output: Expectations POST /users with <custom With matcher> - passed
func (*Expectation) WithBody ¶
func (e *Expectation) WithBody(args ...interface{}) *Expectation
WithBody adds matching conditions against a request's body. See WithQuery for usage instructions
Example ¶
e := Expecter{}
e.ExpectReq("POST", "/posts").WithBody("title", "My first blog post").Once()
body := strings.NewReader(url.Values{
"title": []string{"My first blog post"},
}.Encode())
req := httptest.NewRequest("POST", "/posts", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
e.LogReq(req)
fmt.Println(e.Summary())
Output: Expectations POST /posts with body matching title="My first blog post" - passed
func (*Expectation) WithHeader ¶
func (exp *Expectation) WithHeader(args ...interface{}) *Expectation
WithHeader adds matching conditions against a request's headers
Example ¶
e := Expecter{}
e.ExpectReq("GET", "/foo").WithHeader("Authorization", R("^Bearer .+$"))
req := httptest.NewRequest("GET", "/foo", nil)
req.Header.Set("Authorization", "Bearer foobar")
e.LogReq(req)
fmt.Println(e.Summary())
Output: Expectations GET /foo with header matching Authorization="^Bearer .+$" - passed
func (*Expectation) WithQuery ¶
func (exp *Expectation) WithQuery(args ...interface{}) *Expectation
WithQuery matches against the query string. It has several forms:
WithQuery() // passes if any query string is present
WithQuery("key") // passes ?key and ?key=<any value>
WithQuery("key", "value") // passes ?key=value or &key=value&key=value2
WithQuery(hex.R(`^key$`)) // find keys using regular expressions
WithQuery(hex.P{"key1": "value1", "key2": "value2"}) // match against multiple key/value pairs
WithQuery(hex.P{"key1": hex.R(`^value\d$`)}) // mix-and-match strings, regular expressions and key/value maps
Example ¶
e := Expecter{}
e.ExpectReq("GET", "/search").WithQuery("q", "cats")
e.LogReq(httptest.NewRequest("GET", "/search?q=cats", nil))
fmt.Println(e.Summary())
Output: Expectations GET /search with query string matching q="cats" - passed
Example (InvalidArgument) ¶
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic:", err)
}
}()
e := Expecter{}
// Unrecognized arguments to WithQuery produce a panic.
e.ExpectReq("GET", "/search").WithQuery(123)
Output: Panic: WithQuery: Cannot use value 123 when matching against url.Values
type Expecter ¶
type Expecter struct {
// contains filtered or unexported fields
}
Expecter is the top-level object onto which expectations are made
func (*Expecter) ExpectReq ¶
func (e *Expecter) ExpectReq(method, path interface{}) (exp *Expectation)
ExpectReq adds an Expectation to the stack
func (*Expecter) FailedExpectations ¶
func (e *Expecter) FailedExpectations() (failed []*Expectation)
FailedExpectations returns a list of currently failing
func (*Expecter) HexReport ¶
HexReport logs a summary of passes/fails to the given testing object, and calls t.Errorf with an error message if any expectations failed
func (*Expecter) LogReq ¶
func (e *Expecter) LogReq(req *http.Request) *Expectation
LogReq matches an incoming request against he current tree of Expectations, and returns the matched Expectation if any
func (*Expecter) PassedExpectations ¶
func (e *Expecter) PassedExpectations() (passed []*Expectation)
PassedExpectations returns all passing expectations
func (*Expecter) Summary ¶
Summary returns a summary of all passed/failed expectations and any requests that didn't match
Example ¶
e := Expecter{}
e.ExpectReq("GET", "/status")
e.ExpectReq("POST", "/users")
// Matches one of above expectations, leaving the other unmatched (failing)
e.LogReq(httptest.NewRequest("GET", "/status", nil))
// Extraneous request matches no expectations
e.LogReq(httptest.NewRequest("PATCH", "/items", nil))
fmt.Println(e.Summary())
Output: Expectations GET /status - passed POST /users - failed, no matching requests Unmatched Requests PATCH /items
func (*Expecter) UnmatchedRequests ¶
UnmatchedRequests returns a list of all http.Request objects that didn't match any expectation
type MatchConst ¶
type MatchConst int
MatchConst is used to define some built-in matchers with predefined behavior, namely All or None
const ( // Any matches anything, matching against Any will always return true Any MatchConst = 1 // None matches nothing, matching against None will always return false None MatchConst = 2 )
type P ¶
type P map[interface{}]interface{}
P is a convenience alias for a map of interface{} to interface{}
It's used to add header/body/query string conditions to an expectation:
server.Expect("GET", "/foo").WithQuery(hex.P{"name": "bob", "age": hex.R("^\d+$")})
type Server ¶
Server wraps around (embeds) an httptest.Server, and also embeds an Expecter for making expectations The simplest way of using hex is to use NewServer or NewTLSServer.
func NewServer ¶
NewServer returns a new hex.Server object, wrapping an httptest.Server. Its first argument should be a testing.T, used to report failures. Its second argument is an http.Handler that may be nil.
func NewTLSServer ¶
NewTLSServer returns a new hex.Server object, wrapping na httptest.Server created via NewTLSServer Its first argument should be a testing.T, used to report failures. Its second argument is an http.Handler that may be nil.
type StringMatcher ¶ added in v0.0.5
StringMatcher is used for matching parts of a request that can only ever be strings, such as the HTTP Method, path, query string/header/form keys (not values, which can be arrays), etc.