From 5b0b87b0903d98b680400eaf0b2be5e0afe8eb31 Mon Sep 17 00:00:00 2001
From: steinfletcher
Date: Thu, 3 Sep 2026 12:49:25 +0100
Subject: [PATCH] Rewrite the README as a complete guide to the library
- Replace the dead CircleCI badge with the GitHub Actions one and the
godoc badge with pkg.go.dev.
- Organise the content by task: building requests, asserting on
responses, mocking, sequence diagrams, networking, debugging,
framework integration.
- Document features that were missing: HttpRequest, GraphQL, JSON
request bodies, HeaderPresent/NotPresent, custom assertions and the
IsSuccess helpers, Result.JSON, mock request/response builders,
Times/AnyTimes/UnmatchedMocks, FixedDelay/Timeout, ObserveMocks,
standalone mocks, HttpClient and the parallel-test caveat, Meta
participant names, x/db database recording, custom reporters,
EnableNetworking, TestingT/Ginkgo and Verifier.
- Fix examples that did not compile: apitest.Cookie is not a function
(NewCookie), 'var x :=', and a malformed query collection snippet.
- Add the websocket and database examples to the examples table.
Every Go snippet in the README was extracted and compiled against the
library.
Co-Authored-By: Claude Fable 5.1
---
README.md | 828 +++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 563 insertions(+), 265 deletions(-)
diff --git a/README.md b/README.md
index dc94a78..6f5f9a3 100644
--- a/README.md
+++ b/README.md
@@ -3,208 +3,400 @@
-
-
+
+
# apitest
-A simple and extensible behavioural testing library. Supports mocking external http calls and renders sequence diagrams on completion.
+A simple and extensible behavioural testing library for Go HTTP services. Tests are written the way a client uses the
+API: build a request, send it to the handler, and assert on the response. External HTTP calls can be mocked, and every
+test can render a sequence diagram of what happened.
-In behavioural tests the internal structure of the app is not known by the tests. Data is input to the system and the outputs are expected to meet certain conditions.
+In behavioural tests the internal structure of the app is not known by the tests. Data is input to the system and the
+outputs are expected to meet certain conditions.
+
+- Fluent builders for requests and expectations
+- Works with anything that implements `http.Handler`, or against a running server
+- Declarative mocks for outbound HTTP calls, with matchers for every part of the request
+- Sequence diagrams of each test, including mocked calls and database queries
+- No third party dependencies; integrates with `testing`, Ginkgo and custom assertion libraries
+
+The API is stable. The library is maintained and issues are addressed; feature requests are considered.
Join the conversation at #apitest on [https://gophers.slack.com](https://gophers.slack.com).
Logo by @egonelbre
-Note: The API for apitest is stable and complete - despite the lack of activity this repository is still actively maintained. Any new issues will be addressed. Feature requests will be considered.
-
## Documentation
-Please visit [https://apitest.dev](https://apitest.dev) for the latest documentation.
+This README covers the whole library. The same material with more narrative is at
+[https://apitest.dev](https://apitest.dev), and the API reference is on
+[pkg.go.dev](https://pkg.go.dev/github.com/steinfletcher/apitest).
## Installation
```bash
-go get -u github.com/steinfletcher/apitest
+go get github.com/steinfletcher/apitest
```
+## Quick start
+
+```go
+func TestGetUser(t *testing.T) {
+ apitest.New().
+ Handler(handler).
+ Get("/user/1234").
+ Expect(t).
+ Status(http.StatusOK).
+ Body(`{"id": "1234", "name": "Tate"}`).
+ End()
+}
+```
+
+`Handler` takes any `http.Handler`, so a `http.ServeMux`, a gin engine, an echo instance and so on all work. Everything
+before `Expect(t)` describes the request; everything after it describes the expected response. `End()` runs the test.
+When the expected body is valid JSON it is compared structurally, so key order and whitespace do not matter.
+
## Demo

-## Examples
+## Building the request
-### Framework and library integration examples
+### Method and URL
-| Example | Comment |
-| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
-| [gin](https://github.com/steinfletcher/apitest/tree/master/examples/gin) | popular martini-like web framework |
-| [graphql](https://github.com/steinfletcher/apitest/tree/master/examples/graphql) | using gqlgen.com to generate a graphql server |
-| [gorilla](https://github.com/steinfletcher/apitest/tree/master/examples/gorilla) | the gorilla web toolkit |
-| [iris](https://github.com/steinfletcher/apitest/tree/master/examples/iris) | iris web framework |
-| [echo](https://github.com/steinfletcher/apitest/tree/master/examples/echo) | High performance, extensible, minimalist Go web framework |
-| [fiber](https://github.com/steinfletcher/apitest/tree/master/examples/fiber) | Express inspired web framework written in Go |
-| [httprouter](https://github.com/steinfletcher/apitest/tree/master/examples/httprouter) | High performance HTTP request router that scales well |
-| [mocks](https://github.com/steinfletcher/apitest/tree/master/examples/mocks) | example mocking out external http calls |
-| [sequence diagrams](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams) | generate sequence diagrams from tests. See the [demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/) |
-| [Ginkgo](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo) | Ginkgo BDD test framework|
+`Get`, `Post`, `Put`, `Patch` and `Delete` set the method and URL. Each has an `f` variant that formats the URL, and
+`Method` covers anything else.
-### Companion libraries
+```go
+apitest.Handler(handler).Getf("/user/%s", id)
-| Library | Comment |
-| ----------------------------------------------------------------------- | -----------------------------------------------|
-| [JSONPath](https://github.com/steinfletcher/apitest-jsonpath) | JSONPath assertion addons |
-| [CSS Selectors](https://github.com/steinfletcher/apitest-css-selector) | CSS selector assertion addons |
-| [PlantUML](https://github.com/steinfletcher/apitest-plantuml) | Export sequence diagrams as plantUML |
-| [DynamoDB](https://github.com/steinfletcher/apitest-dynamodb) | Add DynamoDB interactions to sequence diagrams |
+apitest.Handler(handler).Method(http.MethodOptions).URL("/user")
+```
-### Credits
+A ready-made `*http.Request` can be used instead of the builder.
-This library was influenced by the following software packages:
+```go
+req := httptest.NewRequest(http.MethodGet, "/user/1234", nil)
-* [YatSpec](https://github.com/bodar/yatspec) for creating sequence diagrams from tests
-* [MockMVC](https://spring.io) and [superagent](https://github.com/visionmedia/superagent) for the concept and behavioural testing approach
-* [Gock](https://github.com/h2non/gock) for the approach to mocking HTTP services in Go
-* [Baloo](https://github.com/h2non/baloo) for API design
+apitest.Handler(handler).
+ HttpRequest(req).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
-### Code snippets
+### Headers
-#### JSON body matcher
+```go
+apitest.Handler(handler).
+ Get("/hello").
+ Header("Authorization", "Bearer token").
+ Headers(map[string]string{"X-Request-Id": "12345"}).
+ ContentType("application/json").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+### Query parameters
+
+`Query`, `QueryParams` and `QueryCollection` can be combined. `QueryCollection` sets repeated parameters, so
+`map[string][]string{"a": {"b", "c", "d"}}` is encoded as `a=b&a=c&a=d`.
```go
-func TestApi(t *testing.T) {
- apitest.New().
- Handler(handler).
- Get("/user/1234").
- Expect(t).
- Body(`{"id": "1234", "name": "Tate"}`).
- Status(http.StatusOK).
- End()
-}
+apitest.Handler(handler).
+ Get("/hello").
+ QueryParams(map[string]string{"a": "1", "b": "2"}).
+ Query("c", "d").
+ QueryCollection(map[string][]string{"e": {"f", "g"}}).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### JSONPath
+### Cookies
+
+```go
+apitest.Handler(handler).
+ Get("/hello").
+ Cookie("session", "12345").
+ Cookies(apitest.NewCookie("theme").Value("dark").Path("/")).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
-For asserting on parts of the response body JSONPath may be used. A separate module must be installed which provides these assertions - `go get -u github.com/steinfletcher/apitest-jsonpath`. This is packaged separately to keep this library dependency free.
+### Body
-Given the response is `{"a": 12345, "b": [{"key": "c", "value": "result"}]}`
+`Body` sets a raw body. `JSON` sets the body and the `Content-Type` header; it accepts a string, a `[]byte`, or any
+value that can be marshalled. `BodyFromFile` and `JSONFromFile` read the body from disk.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- Expect(t).
- Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
- End()
-}
+apitest.Handler(handler).
+ Post("/user").
+ JSON(map[string]any{"name": "jan", "age": 32}).
+ Expect(t).
+ Status(http.StatusCreated).
+ End()
```
-and `jsonpath.Equals` checks for value equality
+GraphQL requests are built with `GraphQLQuery`, or `GraphQLRequest` when an operation name is needed.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- Expect(t).
- Assert(jsonpath.Equal(`$.a`, float64(12345))).
- End()
-}
+apitest.Handler(handler).
+ Post("/graphql").
+ GraphQLQuery(`query User($id: ID!) { user(id: $id) { name } }`, map[string]any{"id": "1234"}).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Custom assert functions
+### Form data
+
+`FormData` sends an `application/x-www-form-urlencoded` body.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- Expect(t).
- Assert(func(res *http.Response, req *http.Request) error {
- assert.Equal(t, http.StatusOK, res.StatusCode)
- return nil
- }).
- End()
-}
+apitest.Handler(handler).
+ Post("/hello").
+ FormData("a", "1").
+ FormData("b", "2", "3").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Assert cookies
+`MultipartFormData` and `MultipartFile` send `multipart/form-data`. The two form styles cannot be combined in one
+request.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Patch("/hello").
- Expect(t).
- Status(http.StatusOK).
- Cookies(apitest.Cookie("ABC").Value("12345")).
- CookiePresent("Session-Token").
- CookieNotPresent("XXX").
- Cookies(
- apitest.Cookie("ABC").Value("12345"),
- apitest.Cookie("DEF").Value("67890"),
- ).
- End()
+apitest.Handler(handler).
+ Post("/upload").
+ MultipartFormData("description", "holiday photos").
+ MultipartFile("file", "testdata/beach.jpg", "testdata/sunset.jpg").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+Files are read from the OS by default. `UseFS` swaps in any `fs.FS`, such as an in-memory `fstest.MapFS`.
+
+```go
+inMemFS := fstest.MapFS{
+ "audio.wav": &fstest.MapFile{Data: []byte{19, 2, 123, 12, 35, 1}},
}
+
+apitest.Handler(handler).
+ UseFS(inMemFS).
+ Post("/upload").
+ MultipartFile("file", "audio.wav").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Assert headers
+### Basic auth
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- Expect(t).
- Status(http.StatusOK).
- Headers(map[string]string{"ABC": "12345"}).
- End()
+apitest.Handler(handler).
+ Get("/hello").
+ BasicAuth("username", "password").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+### Context
+
+`WithContext` sets the request context, which is how deadlines, cancellation and context values reach the handler.
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+defer cancel()
+
+apitest.Handler(handler).
+ Get("/hello").
+ WithContext(ctx).
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+### Intercept
+
+`Intercept` receives the built `*http.Request` just before it is sent, for changes the builder does not cover.
+
+```go
+apitest.Handler(handler).
+ Intercept(func(req *http.Request) {
+ req.URL.RawQuery = "a[]=xxx&a[]=yyy"
+ }).
+ Get("/hello").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+## Asserting on the response
+
+### Status and body
+
+```go
+apitest.Handler(handler).
+ Get("/user/1234").
+ Expect(t).
+ Status(http.StatusOK).
+ Body(`{"id": "1234", "name": "Tate"}`).
+ End()
+```
+
+A JSON body is compared structurally. Any other body is compared as an exact string. `Bodyf` formats the expected
+body and `BodyFromFile` reads it from disk.
+
+### Headers
+
+```go
+apitest.Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Status(http.StatusOK).
+ Header("Content-Type", "application/json").
+ Headers(map[string]string{"X-Request-Id": "12345"}).
+ HeaderPresent("Etag").
+ HeaderNotPresent("X-Powered-By").
+ End()
+```
+
+### Cookies
+
+Only the fields set on the expected cookie are compared, so `NewCookie("session").Value("12345")` ignores the path,
+expiry and other attributes of the actual cookie.
+
+```go
+apitest.Handler(handler).
+ Patch("/hello").
+ Expect(t).
+ Status(http.StatusOK).
+ Cookie("session", "12345").
+ Cookies(apitest.NewCookie("theme").Value("dark").HttpOnly(true)).
+ CookiePresent("csrf").
+ CookieNotPresent("legacy").
+ End()
+```
+
+### Custom assertions
+
+`Assert` takes a function that receives copies of the response and request and returns an error on failure. It can
+be called several times.
+
+```go
+apitest.Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Assert(func(res *http.Response, req *http.Request) error {
+ if res.Header.Get("X-Rate-Limit") == "" {
+ return errors.New("expected a rate limit header")
+ }
+ return nil
+ }).
+ End()
+```
+
+`apitest.IsSuccess`, `apitest.IsClientError` and `apitest.IsServerError` are ready-made assertions on the status code
+range.
+
+### JSONPath
+
+For asserting on parts of the response body, the separate
+[apitest-jsonpath](https://github.com/steinfletcher/apitest-jsonpath) module provides JSONPath assertions. It is
+packaged separately to keep this library dependency free.
+
+Given the response `{"a": 12345, "b": [{"key": "c", "value": "result"}]}`:
+
+```go
+apitest.Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
+ Assert(jsonpath.Equal(`$.a`, float64(12345))).
+ End()
+```
+
+### The result
+
+`End()` returns a `Result` holding the response, so further checks can be made after the test has run.
+
+```go
+result := apitest.Handler(handler).
+ Get("/user/1234").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+var user struct {
+ Name string `json:"name"`
}
+result.JSON(&user)
```
-#### Mocking external http calls
+## Mocking external HTTP calls
+
+If the handler under test calls other services, those calls can be answered by mocks. A mock describes the request to
+match and the response to return.
```go
var getUser = apitest.NewMock().
- Get("/user/12345").
+ Get("http://users/api/user/12345").
RespondWith().
Body(`{"name": "jon", "id": "1234"}`).
Status(http.StatusOK).
End()
var getPreferences = apitest.NewMock().
- Get("/preferences/12345").
+ Get("http://preferences/api/preferences/12345").
+ Header("Authorization", "Bearer .*").
RespondWith().
- Body(`{"is_contactable": true}`).
+ JSON(map[string]any{"is_contactable": true}).
Status(http.StatusOK).
End()
-func TestApi(t *testing.T) {
+func TestGetUser(t *testing.T) {
apitest.New().
Mocks(getUser, getPreferences).
Handler(handler).
- Get("/hello").
+ Get("/user/12345").
Expect(t).
Status(http.StatusOK).
- Body(`{"name": "jon", "id": "1234"}`).
+ Body(`{"name": "jon", "is_contactable": true}`).
End()
}
```
-It is possible to configure the mock for using `AnyTimes` feature, it allows a mock to be invoked any number of times
-without failing the asserts if it is not used the expected number of times.
-This is very useful in scenarios where the exact number of invocations is not known or not important.
+The request side supports `Header`, `Headers`, `Query`, `QueryParams`, `QueryCollection`, `Body`, `Bodyf`,
+`BodyFromFile`, `BodyRegexp`, `JSON`, `FormData`, `Cookie` and `BasicAuth`, plus `Present` and `NotPresent`
+variants for headers, query parameters, form data and cookies. The response side supports `Status`, `Body`, `Bodyf`,
+`BodyFromFile`, `JSON`, `Header`, `Headers`, `Cookie` and `Cookies`. When no `Content-Type` is set on the response,
+`application/json` is used for a JSON body and `text/plain` otherwise.
+
+Mocks work by replacing the transport of `http.DefaultClient` for the duration of the test. If the code under test
+uses its own `http.Client`, pass it with `HttpClient` so its transport is replaced instead. Because the default
+transport is process-wide, tests that use mocks should not run in parallel unless each provides its own client.
```go
-var getUser := apitest.NewMock().
- Get("http://localhost:8080").
- RespondWith().
- Status(http.StatusOK).
- AnyTimes().
- End()
+apitest.New().
+ HttpClient(client).
+ Mocks(getUser).
+ Handler(handler).
+ Get("/user/12345").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-Note: The `AnyTimes` method can be combined with other methods such as `Times`, but if `AnyTimes` is set, the `Times` setting will have no effect.
-#### How mocks are matched
+### How mocks are matched
Mocks are tried in the order they are passed to `Mocks`, and the first mock whose matchers all pass is used. Each mock
is used once unless `Times` or `AnyTimes` is set. The built-in matchers behave as follows:
@@ -219,234 +411,340 @@ is used once unless `Times` or `AnyTimes` is set. The built-in matchers behave a
* **Body** must equal the mock body, or be equivalent JSON. `BodyRegexp` matches the body against a regular expression.
* **Cookies** and **basic auth** must equal the mock's.
-Custom matchers can be added with `AddMatcher`.
+When no mock matches, the call fails with an error listing why each mock was rejected. Run the test with `Debug()` to
+see it.
-#### Generating sequence diagrams from tests
+### Custom matchers
-```go
+`AddMatcher` adds a matcher alongside the built-in ones. It receives the actual request and the mock's request
+specification, and returns an error when the request should not match.
-func TestApi(t *testing.T) {
- apitest.New().
- Report(apitest.SequenceDiagram()).
- Mocks(getUser, getPreferences).
- Handler(handler).
- Get("/hello").
- Expect(t).
- Status(http.StatusOK).
- Body(`{"name": "jon", "id": "1234"}`).
- End()
-}
+```go
+var getPreferences = apitest.NewMock().
+ Get("/preferences/12345").
+ AddMatcher(func(r *http.Request, mr *apitest.MockRequest) error {
+ if r.URL.Scheme != "https" {
+ return errors.New("expected an https request")
+ }
+ return nil
+ }).
+ RespondWith().
+ Status(http.StatusOK).
+ End()
```
-It is possible to override the default storage location by passing the formatter instance `Report(apitest.SequenceDiagram(".sequence-diagrams"))`.
-You can bring your own formatter too if you want to produce custom output. By default a sequence diagram is rendered on a html page. See the [demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/)
+### Times and unused mocks
-#### Debugging http requests and responses generated by api test and any mocks
+By default a mock answers one request. `Times(n)` makes it answer `n` requests and fails the test if it is called
+fewer times. `AnyTimes` lets a mock answer any number of calls, including none, and takes precedence over `Times`.
```go
-func TestApi(t *testing.T) {
- apitest.New().
- Debug().
- Handler(handler).
- Get("/hello").
- Expect(t).
- Status(http.StatusOK).
- End()
-}
+var getUser = apitest.NewMock().
+ Get("http://users/api/user/12345").
+ RespondWith().
+ Status(http.StatusOK).
+ Times(2).
+ End()
+
+var healthCheck = apitest.NewMock().
+ Get("http://users/health").
+ RespondWith().
+ Status(http.StatusOK).
+ AnyTimes().
+ End()
```
-#### Provide basic auth in the request
+Mocks that were never called are reported on the result.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- BasicAuth("username", "password").
- Expect(t).
- Status(http.StatusOK).
- End()
+result := apitest.New().
+ Mocks(getUser, getPreferences).
+ Handler(handler).
+ Get("/user/12345").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+
+for _, unmatched := range result.UnmatchedMocks() {
+ t.Logf("mock not called: %s", unmatched.URL.String())
}
```
-#### Pass a custom context to the request
+### Slow and failing dependencies
+
+`Timeout` makes the mocked call fail with a timeout error. `FixedDelay` waits the given number of milliseconds before
+responding, and only takes effect when the test enables delays with `EnableMockResponseDelay`.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- WithContext(context.TODO()).
+var slowUsers = apitest.NewMock().
+ Get("http://users/api/user/12345").
+ RespondWith().
+ FixedDelay(500).
+ Status(http.StatusOK).
+ End()
+
+var brokenPreferences = apitest.NewMock().
+ Get("http://preferences/api/preferences/12345").
+ RespondWith().
+ Timeout().
+ End()
+
+func TestGetUser_WhenDependenciesAreSlow(t *testing.T) {
+ apitest.New().
+ EnableMockResponseDelay().
+ Mocks(slowUsers, brokenPreferences).
+ Handler(handler).
+ Get("/user/12345").
Expect(t).
- Status(http.StatusOK).
+ Status(http.StatusGatewayTimeout).
End()
}
```
-#### Test handler timeouts
+### Observing mock calls
-Wrap the handler in the standard library's `http.TimeoutHandler` when passing it to apitest. A handler that
-overruns the timeout produces the `503` status and body that `TimeoutHandler` generates, which can be asserted on
-as usual. To check that a handler honours a deadline instead, pass a context with a deadline using `WithContext`.
+`ObserveMocks` is called with the request and response of every mocked call.
```go
-func TestApi(t *testing.T) {
- handler := http.TimeoutHandler(slowHandler, 50*time.Millisecond, "request timed out")
-
- apitest.Handler(handler).
- Get("/slow").
- Expect(t).
- Status(http.StatusServiceUnavailable).
- Body("request timed out").
- End()
-}
+apitest.New().
+ ObserveMocks(func(res *http.Response, req *http.Request, apiTest *apitest.APITest) {
+ t.Logf("%s %s -> %d", req.Method, req.URL, res.StatusCode)
+ }).
+ Mocks(getUser).
+ Handler(handler).
+ Get("/user/12345").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Provide cookies in the request
+### Standalone mocks
+
+Mocks can also be used outside an apitest test, for example in a unit test of an HTTP client. `EndStandalone` and
+`NewStandaloneMocks(...).End()` install the mocks and return a function that removes them.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- Cookies(apitest.Cookie("ABC").Value("12345")).
- Expect(t).
+func TestUserClient(t *testing.T) {
+ reset := apitest.NewMock().
+ Get("http://users/api/user/12345").
+ RespondWith().
+ Body(`{"name": "jon"}`).
Status(http.StatusOK).
- End()
+ EndStandalone()
+ defer reset()
+
+ user, err := userClient.Get("12345")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if user.Name != "jon" {
+ t.Fatalf("unexpected user %+v", user)
+ }
}
```
-#### Provide headers in the request
+Use `HttpClient` on the mock to target a specific client, and `Debug` on the mock to log the matching.
+
+## Sequence diagrams and reports
+
+`Report` renders every test into a report once it completes. The built-in `SequenceDiagram` formatter writes an HTML
+page per test with a sequence diagram of the request, any mocked calls, and the response, along with the full wire
+representation of each message. See the
+[demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/).
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Delete("/hello").
- Headers(map[string]string{"My-Header": "12345"}).
+func TestGetUser(t *testing.T) {
+ apitest.New("gets the user").
+ Report(apitest.SequenceDiagram()).
+ Mocks(getUser, getPreferences).
+ Handler(handler).
+ Get("/user/12345").
Expect(t).
Status(http.StatusOK).
End()
}
```
-#### Provide query parameters in the request
+The name passed to `New` appears in the report. Diagrams are written to `.sequence` by default; pass a path to
+`SequenceDiagram` to change that. The participants are labelled `cli` and `sut` unless `Meta` provides names.
+
+```go
+apitest.New("gets the user").
+ Report(apitest.SequenceDiagram(".sequence-diagrams")).
+ Meta(map[string]any{
+ "consumerName": "web-app",
+ "systemUnderTestName": "user-api",
+ }).
+ Handler(handler).
+ Get("/user/12345").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
+```
+
+### Database calls in diagrams
-`Query`, `QueryParams` and `QueryCollection` can all be used in combination
+The `x/db` package wraps a `database/sql` driver so that queries made during the test appear in the diagram. Wrap the
+driver with a `Recorder`, register the wrapped driver, open the database through it, and pass the same recorder to the
+test.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- QueryParams(map[string]string{"a": "1", "b": "2"}).
- Query("c", "d").
+var recorder = apitest.NewTestRecorder()
+
+func init() {
+ sql.Register("recorded-sqlite3", apitestdb.WrapWithRecorder("sqlite3", recorder))
+}
+
+func TestGetUser(t *testing.T) {
+ db, err := sql.Open("recorded-sqlite3", "./users.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+
+ apitest.New("gets the user").
+ Recorder(recorder).
+ Report(apitest.SequenceDiagram()).
+ Handler(newApp(db)).
+ Get("/user/12345").
Expect(t).
Status(http.StatusOK).
End()
}
```
-Providing `{"a": {"b", "c", "d"}` results in parameters encoded as `a=b&a=c&a=d`.
-`QueryCollection` can be used in combination with `Query`
+`WrapConnectorWithRecorder` does the same for a `driver.Connector`. See the
+[sqlite](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-sqlite-database),
+[mysql](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-mysql-database) and
+[postgres](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-postgres-database)
+examples.
+
+### Custom reports
+
+Anything implementing `ReportFormatter` can be passed to `Report`. It receives the `Recorder`, which holds the title,
+the meta data and the ordered list of events, so reports can be rendered in any format. The
+[PlantUML](https://github.com/steinfletcher/apitest-plantuml) companion library is one example. Custom events can be
+added to the recorder from your own code with `AddMessageRequest` and `AddMessageResponse`.
+
+## Testing a running server
+
+`EnableNetworking` sends the request over the network instead of to a handler. Pass a client to control timeouts,
+cookies and redirects.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Get("/hello").
- QueryCollection(map[string][]string{"a": {"b", "c", "d"}}).
- Expect(t).
- Status(http.StatusOK).
- End()
-}
+client := &http.Client{Timeout: 5 * time.Second}
+
+apitest.New().
+ EnableNetworking(client).
+ Get("http://localhost:8080/health").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Provide a url encoded form body in the request
+## Debugging
+
+`Debug` prints the wire representation of the inbound request, the final response, and every mocked call, together
+with the reasons a request failed to match any mock.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Post("/hello").
- FormData("a", "1").
- FormData("b", "2").
- FormData("b", "3").
- FormData("c", "4", "5", "6").
- Expect(t).
- Status(http.StatusOK).
- End()
-}
+apitest.New().
+ Debug().
+ Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Provide a multipart/form-data
+`Observe` gives programmatic access to the same data.
```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Post("/hello").
- MultipartFormData("a", "1", "2").
- MultipartFile("file", "path/to/some.file1", "path/to/some.file2").
- Expect(t).
- Status(http.StatusOK).
- End()
-}
+apitest.New().
+ Observe(func(res *http.Response, req *http.Request, apiTest *apitest.APITest) {
+ // inspect the copies of res and req
+ }).
+ Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Status(http.StatusOK).
+ End()
```
-#### Provide a multipart/form-data with custom filesystem
+## Testing handler timeouts
+
+Wrap the handler in the standard library's `http.TimeoutHandler` when passing it to apitest. A handler that overruns
+the timeout produces the `503` status and body that `TimeoutHandler` generates, which can be asserted on as usual. To
+check that a handler honours a deadline instead, pass a context with a deadline using `WithContext`.
```go
-inMemFS := fstest.MapFS{
- "audio.wav": &fstest.MapFile{
- Data: []byte{19,2,123,12,35,1},
- Mode: fs.FileMode(0644),
- ModTime: time.Now(),
- },
- "audio.mp3": &fstest.MapFile{
- Data: []byte{21,13,88,123,9,8},
- Mode: fs.FileMode(0644),
- ModTime: time.Now(),
- },
-}
+func TestSlowEndpoint(t *testing.T) {
+ handler := http.TimeoutHandler(slowHandler, 50*time.Millisecond, "request timed out")
-func TestApi(t *testing.T) {
apitest.Handler(handler).
- UseFS(inMemFS).
- Post("/hello").
- MultipartFormData("a", "1", "2").
- MultipartFile("file", "audio.wav", "audio.mp3").
+ Get("/slow").
Expect(t).
- Status(http.StatusOK).
+ Status(http.StatusServiceUnavailable).
+ Body("request timed out").
End()
}
```
+## Test framework integration
+
+`Expect` accepts any value implementing `TestingT`, which is the subset of `*testing.T` that apitest needs:
+`Errorf`, `Fatal` and `Fatalf`. Ginkgo's `GinkgoT()` satisfies it, so apitest works inside Ginkgo specs; see the
+[Ginkgo example](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo).
-#### Capture the request and response data
+Assertions are performed through the `Verifier` interface. `Verifier` swaps in a different implementation, for example
+`NoopVerifier` to run a test without failing it, or an adapter over your preferred assertion library.
```go
-func TestApi(t *testing.T) {
- apitest.New().
- Observe(func(res *http.Response, req *http.Request, apiTest *apitest.APITest) {
- // do something with res and req
- }).
- Handler(handler).
- Get("/hello").
- Expect(t).
- Status(http.StatusOK).
- End()
-}
+apitest.New().
+ Verifier(apitest.NoopVerifier{}).
+ Handler(handler).
+ Get("/hello").
+ Expect(t).
+ Status(http.StatusTeapot).
+ End()
```
-#### Intercept the request
+## Examples
-This is useful for mutating the request before it is sent to the system under test.
+### Framework and library integration examples
-```go
-func TestApi(t *testing.T) {
- apitest.Handler(handler).
- Intercept(func(req *http.Request) {
- req.URL.RawQuery = "a[]=xxx&a[]=yyy"
- }).
- Get("/hello").
- Expect(t).
- Status(http.StatusOK).
- End()
-}
-```
+| Example | Comment |
+| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| [gin](https://github.com/steinfletcher/apitest/tree/master/examples/gin) | popular martini-like web framework |
+| [graphql](https://github.com/steinfletcher/apitest/tree/master/examples/graphql) | using gqlgen.com to generate a graphql server |
+| [gorilla](https://github.com/steinfletcher/apitest/tree/master/examples/gorilla) | the gorilla web toolkit |
+| [iris](https://github.com/steinfletcher/apitest/tree/master/examples/iris) | iris web framework |
+| [echo](https://github.com/steinfletcher/apitest/tree/master/examples/echo) | High performance, extensible, minimalist Go web framework |
+| [fiber](https://github.com/steinfletcher/apitest/tree/master/examples/fiber) | Express inspired web framework written in Go |
+| [httprouter](https://github.com/steinfletcher/apitest/tree/master/examples/httprouter) | High performance HTTP request router that scales well |
+| [Ginkgo](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo) | Ginkgo BDD test framework |
+| [mocks](https://github.com/steinfletcher/apitest/tree/master/examples/mocks) | mocking out external http calls, including a custom matcher |
+| [websockets](https://github.com/steinfletcher/apitest/tree/master/examples/websockets) | a websocket handler under test |
+| [sequence diagrams](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams) | generate sequence diagrams from tests. See the [demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/) |
+| [sqlite](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-sqlite-database), [mysql](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-mysql-database), [postgres](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-postgres-database) | database queries recorded in sequence diagrams |
+
+### Companion libraries
+
+| Library | Comment |
+| ----------------------------------------------------------------------- | -----------------------------------------------|
+| [JSONPath](https://github.com/steinfletcher/apitest-jsonpath) | JSONPath assertion addons |
+| [CSS Selectors](https://github.com/steinfletcher/apitest-css-selector) | CSS selector assertion addons |
+| [PlantUML](https://github.com/steinfletcher/apitest-plantuml) | Export sequence diagrams as plantUML |
+| [DynamoDB](https://github.com/steinfletcher/apitest-dynamodb) | Add DynamoDB interactions to sequence diagrams |
+
+### Credits
+
+This library was influenced by the following software packages:
+
+* [YatSpec](https://github.com/bodar/yatspec) for creating sequence diagrams from tests
+* [MockMVC](https://spring.io) and [superagent](https://github.com/visionmedia/superagent) for the concept and behavioural testing approach
+* [Gock](https://github.com/h2non/gock) for the approach to mocking HTTP services in Go
+* [Baloo](https://github.com/h2non/baloo) for API design
## Contributing