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", "user@example.com").Do(func() { client.Create(User{ Name: "User McUser", Email: "user@example.com", }) }) fmt.Println(service.Summary()) }
Output: Expectations GET /users/123 - passed POST /users with body matching name="User McUser"body matching email="user@example.com" - 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.