hat
hat copied to clipboard
HTTP API testing for Go
hat
hat is an HTTP API testing framework for Go.
It's based on composable, reusable response assertions, and request modifiers. It can dramatically reduce API testing
code, while improving clarity of test code and test output. It leans on the standard net/http package
as much as possible.
Import as go.coder.com/hat.
Example
Let's test that twitter is working:
func TestTwitter(tt *testing.T) {
t := hat.New(tt, "https://twitter.com")
t.Get(
hat.Path("/realDonaldTrump"),
).Send(t).Assert(t,
asshat.StatusEqual(http.StatusOK),
asshat.BodyMatches(`President`),
)
}
Table of Contents generated with DocToc
- hat
- Example
- Basic Concepts
- Creating Requests
- Sending Requests
- Reading Responses
- Competitive Comparison
- API Symbols
- LoC
- net/http
- Chaining APIs
- Design Patterns
- Format Agnostic
- Minimal API
- testing.TB instead of *hat.T
Basic Concepts
Creating Requests
hat's entrypoint is its New method
func New(t *testing.T, baseURL string) *T
which returns a hat.T that embeds a testing.T, and provides a bunch of methods such as
Get, Post, and Patch to generate HTTP requests. Each request method looks like
func (t *T) Get(opts ...RequestOption) Request
RequestOption has the signature
type RequestOption func(t testing.TB, req *http.Request)
Sending Requests
Each request modifies the request however it likes. A few common RequestOptions are provided
in the hat package.
Once the request is built, it can be sent
func (r Request) Send(t *T) *Response
or cloned
func (r Request) Clone(t *T, opts ...RequestOption) Request
Cloning is useful when a test is making a slight modification of a complex request.
Reading Responses
Once you've sent the request, you're given a hat.Response. The Response should be asserted.
func (r Response) Assert(t testing.TB, assertions ...ResponseAssertion) Response
ResponseAssertion looks like
type ResponseAssertion func(t testing.TB, r Response)
A bunch of pre-made response assertions are available in
the asshat package.
Competitive Comparison
It's difficult to say objectively which framework is the best. But, no existing framework satisfied us, and we're happy with hat.
| Library | API Symbols | LoC | net/http |
Custom Assertions/Modifiers |
|---|---|---|---|---|
| hat | 24 | 410 | :heavy_check_mark: | :heavy_check_mark: |
| github.com/gavv/httpexpect | 280 | 10042 | :heavy_multiplication_x: | :warning: (Chaining API) |
| github.com/h2non/baloo | 91 | 2146 | :heavy_multiplication_x: | :warning: (Chaining API) |
| github.com/h2non/gock | 122 | 2957 | :heavy_multiplication_x: | :warning: (Chaining API) |
LoC was calculated with cloc.
Will add more columns and libraries on demand.
API Symbols
Smaller APIs are easier to use and tend to be less opinionated.
LoC
Smaller codebases have less bugs and are easier to contribute to.
net/http
We prefer to use net/http.Request and net/http.Response so we can reuse the knowledge
we already have. Also, we want to reimplement its surface area.
Chaining APIs
Chaining APIs look like
m.GET("/some-path").
Expect().
Status(http.StatusOK)
We dislike them because they make custom assertions and request modifiers a second-class citizen to the assertions and modifiers of the package. This encourages the framework's API to bloat, and discourages abstraction on part of the user.
Design Patterns
Format Agnostic
hat makes no assumption about the structure of your API, request or response encoding, or
the size of the requests or responses.
Minimal API
hat and asshat maintains a very small base of helpers. We think of the provided helpers as primitives for organization and application-specific helpers.
Always Fatal
While some assertions don't invalidate the test, we typically don't mind if they fail the test immediately.
To avoid the API complexity of selecting
between Errors and Fatals, we fatal all the time.
testing.TB instead of *hat.T
When porting your code over to hat, it's better to accept a testing.TB than a *hat.T or a *testing.T.
Only accept a *hat.T when the function is creating additional requests. This makes the code less coupled,
while clarifying the scope of the helper.
This pattern is used in hat itself. The ResponseAssertion type and the Assert function accept
testing.TB instead of a concrete *hat.T or *testing.T. At first glance, it seems like wherever
the caller is using a ResponseAssertion or Assert, they would have a *hat.T.
In reality, this choice lets consumers hide the initialization of hat.T behind a helper function. E.g:
func TestSomething(t *testing.T) {
makeRequest(t,
hat.Path("/test"),
).Assert(t,
asshat.StatusEqual(t, http.StatusOK),
)
}