# Anatomy of a test (/docs/anatomy)
```go title="user_test.go"
func TestGetUser(t *testing.T) {
apitest.New("gets a user").
// 1. configuration
Mocks(getUserMock, getPreferencesMock).
Handler(newUserAPI()).
// 2. request
Get("/user/1234").
// 3. expectations
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon", "is_contactable": true}`).
End()
}
```
## 1. Configuration [#1-configuration]
`apitest.New` starts a test. The optional name appears in [reports](/docs/reports/sequence-diagrams). Configuration methods return the test, so they can be chained in any order:
| Method | Purpose |
| --- | --- |
| `Handler`, `HandlerFunc` | The handler under test. See [requests](/docs/requests). |
| `Mocks` | Answers for the outbound HTTP calls the handler makes. See [mocks](/docs/mocks). |
| `HttpClient` | Replace the transport of your own `http.Client` instead of `http.DefaultClient`. |
| `Report`, `Recorder`, `Meta` | Render the test as a [sequence diagram](/docs/reports/sequence-diagrams) or a [custom report](/docs/reports/custom-reports). |
| `Debug` | Print every request and response on the wire. See [debugging](/docs/guides/debugging). |
| `Observe`, `ObserveMocks` | Hooks that receive the final and mocked requests and responses. |
| `Intercept` | Change the built request before it is sent. See [intercept](/docs/requests/intercept). |
| `EnableNetworking` | Send the request over the network to a running server. See [end-to-end tests](/docs/guides/end-to-end). |
| `EnableMockResponseDelay` | Honour `FixedDelay` on mocks. See [failures and latency](/docs/mocks/failures-and-latency). |
| `Verifier` | Replace the assertion implementation. See [test frameworks](/docs/guides/test-frameworks). |
| `UseFS`, `Host` | The file system for multipart uploads, and the request `Host`. |
`apitest.Handler(h)` and `apitest.HandlerFunc(f)` are shorthand for `apitest.New().Handler(h)` when no other configuration is needed.
The mocks in the example are package variables, so several tests can share them:
```go title="mocks_test.go"
var getUserMock = apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End()
var getPreferencesMock = apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Status(http.StatusOK).
JSON(map[string]any{"is_contactable": true}).
End()
```
## 2. Request [#2-request]
A method such as `Get`, `Post` or `Method` ends the configuration and starts the request. The request builder sets headers, query parameters, cookies, the body and so on. See [building requests](/docs/requests).
## 3. Expectations [#3-expectations]
`Expect(t)` ends the request and starts the expectations. It takes the `*testing.T`, or anything that satisfies [`apitest.TestingT`](/docs/guides/test-frameworks).
The expectation builder asserts on the status, headers, cookies and body, and accepts [custom assertions](/docs/assertions/custom). `End()` runs the test and returns a [`Result`](/docs/assertions/result).
Nothing runs until `End()` is called. A chain that stops at an expectation compiles, makes no request, and passes.
## The application in these docs [#the-application-in-these-docs]
Most examples test a small user API. `GET /user/{id}` fetches the user from a users service and their contact preferences from a preferences service, then combines the two. Both services are called over HTTP with `http.DefaultClient`.
```go title="user_api.go"
type User struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
IsContactable bool `json:"is_contactable"`
}
func newUserAPI() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /user/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var user User
if err := getJSON(r.Context(), "http://users/api/user/"+id, &user); err != nil {
writeError(w, err)
return
}
var prefs struct {
IsContactable bool `json:"is_contactable"`
}
if err := getJSON(r.Context(), "http://preferences/api/preferences/"+id, &prefs); err != nil {
writeError(w, err)
return
}
user.IsContactable = prefs.IsContactable
writeJSON(w, http.StatusOK, user)
})
mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil || user.Name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
var created User
if err := postJSON(r.Context(), "http://users/api/user", user, &created); err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusCreated, created)
})
return mux
}
```
# Body (/docs/assertions/body)
## JSON bodies [#json-bodies]
When the expected body is valid JSON, it is compared with the actual body structurally. Key order, whitespace and a trailing newline from `json.Encoder` make no difference:
```go
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{
"name": "jon",
"is_contactable": true,
"id": "1234"
}`).
End()
```
On a mismatch, the failure shows a diff of the decoded values. See [reading a failure](/docs/getting-started#reading-a-failure).
## Other bodies [#other-bodies]
Any other body is compared as an exact string, so every character matters, including trailing newlines:
```go
apitest.Handler(handler).
Get("/ping").
Expect(t).
Status(http.StatusOK).
Body("pong").
End()
```
## Formatted bodies [#formatted-bodies]
`Bodyf` formats the expected body with `fmt.Sprintf`, which helps when the body includes generated values:
```go
apitest.Handler(handler).
Getf("/user/%s", id).
Expect(t).
Bodyf(`{"id": "%s", "name": "jon", "is_contactable": true}`, id).
End()
```
## Golden files [#golden-files]
`BodyFromFile` reads the expected body from a file. JSON files are compared structurally, like `Body`:
```go
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
BodyFromFile("testdata/user.json").
End()
```
```json title="testdata/user.json"
{
"id": "1234",
"name": "jon",
"is_contactable": true
}
```
## Partial matches [#partial-matches]
`Body` compares the whole body. When the response contains values that change between runs, such as timestamps or generated IDs, assert on the stable parts with [JSONPath](/docs/assertions/jsonpath), or write a [custom assertion](/docs/assertions/custom).
# Cookies (/docs/assertions/cookies)
```go
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Cookie("theme", "dark").
Cookies(
apitest.NewCookie("session").
Value("12345").
Path("/").
HttpOnly(true),
).
CookiePresent("csrf").
CookieNotPresent("legacy").
End()
```
| Method | Passes when |
| --- | --- |
| `Cookie(name, value)` | A cookie with the name was set, with that value. |
| `Cookies(...*apitest.Cookie)` | Each cookie was set, and matches every field you set on it. |
| `CookiePresent(name)` | A cookie with the name was set, whatever its value. |
| `CookieNotPresent(name)` | No cookie with the name was set. |
## Only the fields you set are compared [#only-the-fields-you-set-are-compared]
`apitest.NewCookie(name)` builds the expected cookie. Fields you do not set are ignored, so `NewCookie("session").Value("12345")` passes whatever the cookie's path, expiry or flags.
Set more fields to check them too:
```go
apitest.Handler(handler).
Post("/login").
Expect(t).
Cookies(
apitest.NewCookie("session").
Value("12345").
Path("/").
Domain("example.com").
Expires(expires).
MaxAge(3600).
Secure(true).
HttpOnly(true),
).
End()
```
The fields that can be checked are `Value`, `Path`, `Domain`, `Expires`, `MaxAge`, `Secure` and `HttpOnly`.
## Cookies with changing values [#cookies-with-changing-values]
Session IDs and CSRF tokens change on every run. Check that they were set with `CookiePresent`, and check their attributes with `Cookies` while leaving out `Value`.
# Custom assertions (/docs/assertions/custom)
`Assert` takes a function that receives the response and the request, and returns an error when the check fails:
```go
apitest.Handler(handler).
Get("/user/1234").
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()
```
```go title="apitest.Assert"
type Assert func(*http.Response, *http.Request) error
```
- The function receives copies of the response and request, so it can read the body without affecting other checks.
- `Assert` can be called any number of times. The functions run after the built-in checks, in the order they were added.
- A returned error is reported as a test failure with `t.Errorf`, and the remaining checks still run.
## Reusable assertions [#reusable-assertions]
Assertions are plain functions, so a function that returns one can be shared across tests and packages:
```go
// hasCacheControl asserts on the Cache-Control header of the response.
func hasCacheControl(want string) apitest.Assert {
return func(res *http.Response, _ *http.Request) error {
if got := res.Header.Get("Cache-Control"); got != want {
return fmt.Errorf("Cache-Control: got %q, want %q", got, want)
}
return nil
}
}
func TestCaching(t *testing.T) {
apitest.Handler(cachedHandler).
Get("/static/app.js").
Expect(t).
Status(http.StatusOK).
Assert(hasCacheControl("public, max-age=31536000")).
End()
}
```
The [JSONPath](/docs/assertions/jsonpath) and [CSS selector](/docs/reference/companion-libraries#css-selectors) modules are built this way.
## Ready-made assertions [#ready-made-assertions]
`apitest.IsSuccess`, `apitest.IsClientError` and `apitest.IsServerError` assert that the status code is in a range. See [status](/docs/assertions#status).
# Headers (/docs/assertions/headers)
```go
apitest.Handler(handler).
Get("/user/1234").
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()
```
| Method | Passes when |
| --- | --- |
| `Header(key, value)` | The header exists and one of its values equals `value`. |
| `Headers(map[string]string)` | Each entry passes as it would with `Header`. |
| `HeaderPresent(key)` | The header exists, whatever its value, including an empty value. |
| `HeaderNotPresent(key)` | The header does not exist. |
Values are compared exactly. A header with several values, such as `Vary: Accept` and `Vary: Origin`, passes `Header("Vary", "Origin")` because one of its values matches.
Header names are canonicalised, so `content-type` and `Content-Type` are the same header.
To match a header value with a pattern, or to compare a value such as a date loosely, use a [custom assertion](/docs/assertions/custom).
# Asserting on responses (/docs/assertions)
`Expect(t)` ends the request and starts the expectations. Each expectation method adds a check, and `End()` sends the request and runs them.
```go
apitest.Handler(handler).
Get("/user/1234").
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()
```
## How expectations run [#how-expectations-run]
- **Only what you set is checked.** With no `Status` the status code is not checked, and with no `Body` the body is not checked.
- **Every failure is reported.** Failed checks call `t.Errorf`, so one run reports every mismatch rather than stopping at the first.
- **Checks run in a fixed order.** Mock call counts come first, then the status, body, headers, cookies, and finally custom `Assert` functions in the order they were added.
## Status [#status]
`Status` asserts on the status code. Use the `net/http` constants for readability:
```go
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
To accept a range of codes, use one of the ready-made assertions with [`Assert`](/docs/assertions/custom):
```go
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Assert(apitest.IsSuccess).
End()
```
| Assertion | Passes for |
| --- | --- |
| `apitest.IsSuccess` | 200 to 399 |
| `apitest.IsClientError` | 400 to 499 |
| `apitest.IsServerError` | 500 and above |
## In this section [#in-this-section]
# JSONPath (/docs/assertions/jsonpath)
JSONPath assertions check parts of a JSON body, which is useful when the response is large or contains values that change between runs. They live in a separate module, which keeps apitest itself free of dependencies.
```bash
go get github.com/steinfletcher/apitest-jsonpath
```
```go
import jsonpath "github.com/steinfletcher/apitest-jsonpath"
```
Each function returns an assertion to pass to `Assert`:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Status(http.StatusOK).
Assert(jsonpath.Equal(`$.author.name`, "jon")).
Assert(jsonpath.Contains(`$.tags`, "testing")).
Assert(jsonpath.Len(`$.comments`, 2)).
Assert(jsonpath.NotPresent(`$.author.password`)).
End()
```
The examples on this page use this response:
```json title="GET /articles/12345"
{
"id": 12345,
"title": "Testing Go HTTP services",
"slug": "testing-go-http-services",
"published": true,
"author": { "name": "jon", "email": "jon@example.com" },
"tags": ["go", "testing", "http"],
"comments": [
{ "id": 1, "author": "ola", "approved": true },
{ "id": 2, "author": "kim", "approved": false }
]
}
```
## Equal and NotEqual [#equal-and-notequal]
`Equal` passes when the expression selects a value equal to the expected one:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Equal(`$.title`, "Testing Go HTTP services")).
Assert(jsonpath.Equal(`$.published`, true)).
// JSON numbers decode as float64
Assert(jsonpath.Equal(`$.id`, float64(12345))).
End()
```
The body is decoded with `encoding/json`, so every JSON number becomes a `float64`. Compare numbers with `float64(12345)`, not `12345`.
Objects and arrays are compared as `map[string]any` and `[]any`:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Equal(`$.author`, map[string]any{
"name": "jon",
"email": "jon@example.com",
})).
End()
```
`NotEqual` is the inverse:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.NotEqual(`$.author.name`, "ola")).
End()
```
## Contains [#contains]
`Contains` passes when the expression selects an array that includes the expected value. Filter expressions select values from arrays of objects:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Contains(`$.tags`, "go")).
// filter expressions select from arrays of objects
Assert(jsonpath.Contains(`$.comments[?@.approved == true].author`, "ola")).
End()
```
## Present and NotPresent [#present-and-notpresent]
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Present(`$.slug`)).
Assert(jsonpath.NotPresent(`$.author.password`)).
End()
```
`Present` passes only for a non-empty value. A field that is `null`, `false`, `0`, an empty string or an empty array counts as not present, and passes `NotPresent`. To check such a field, use `Equal`, for example `jsonpath.Equal("$.published", false)`.
## Matches [#matches]
`Matches` checks a string, number or boolean against a regular expression. Numbers and booleans are formatted with `fmt.Sprint` before matching.
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Matches(`$.slug`, `^[a-z0-9-]+$`)).
Assert(jsonpath.Matches(`$.author.email`, `@example\.com$`)).
Assert(jsonpath.Matches(`$.id`, `^\d{5}$`)).
End()
```
## Length [#length]
`Len` checks the length of an array, object or string. `GreaterThan` and `LessThan` set bounds on it:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(jsonpath.Len(`$.tags`, 3)).
Assert(jsonpath.GreaterThan(`$.tags`, 2)).
Assert(jsonpath.LessThan(`$.comments`, 10)).
End()
```
Despite their names, `GreaterThan(expr, n)` passes when the length is at least `n`, and `LessThan(expr, n)` passes when it is at most `n`.
## Chain and Root [#chain-and-root]
`Chain` groups several assertions into one:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(
jsonpath.Chain().
Equal(`$.author.name`, "jon").
NotEqual(`$.author.name`, "ola").
Present(`$.slug`).
Contains(`$.tags`, "http").
End(),
).
End()
```
`Root` is a chain whose expressions are relative to a common prefix, which removes repetition. A dot is inserted between the root and each expression, unless the expression starts with `[` or `.`, so array elements can be addressed directly:
```go
apitest.Handler(handler).
Get("/articles/12345").
Expect(t).
Assert(
jsonpath.Root(`$.author`).
Equal(`name`, "jon").
Matches(`email`, `^jon@`).
End(),
).
Assert(
jsonpath.Root(`$.comments`).
Equal(`[0].author`, "ola").
Equal(`[1].approved`, false).
End(),
).
End()
```
Chains support `Equal`, `NotEqual`, `Contains`, `Present`, `NotPresent` and `Matches`.
## JWT [#jwt]
`JWTHeaderEqual` and `JWTPayloadEqual` decode a JSON Web Token from the response and assert on its header or claims. They do not verify the signature.
The first argument selects the token from the response:
```go
func fromAuthHeader(res *http.Response) (string, error) {
return res.Header.Get("Authorization"), nil
}
```
```go
apitest.Handler(handler).
Post("/login").
Expect(t).
Status(http.StatusOK).
Assert(jsonpath.JWTHeaderEqual(fromAuthHeader, `$.alg`, "HS256")).
Assert(jsonpath.JWTPayloadEqual(fromAuthHeader, `$.sub`, "1234567890")).
Assert(jsonpath.JWTPayloadEqual(fromAuthHeader, `$.name`, "John Doe")).
End()
```
## Matching mock requests [#matching-mock-requests]
The `apitest-jsonpath/mocks` package provides the same assertions as mock matchers, for matching the JSON body of requests your handler sends to a mock. See [custom matchers](/docs/mocks/custom-matchers#jsonpath-matchers).
## Expression syntax [#expression-syntax]
Expressions are evaluated by [PaesslerAG/jsonpath](https://github.com/PaesslerAG/jsonpath). Common forms:
| Expression | Selects |
| --- | --- |
| `$.author.name` | A nested field. |
| `$.tags[0]` | An array element. |
| `$.comments[*].author` | A field of every element. |
| `$.comments[?@.approved == true].author` | A field of the elements that match a filter. |
| `$..id` | Every `id` at any depth. |
# The result (/docs/assertions/result)
`End()` runs the test and returns an `apitest.Result`:
```go title="apitest.Result"
type Result struct {
Response *http.Response
// unexported fields
}
func (r Result) JSON(v any)
func (r Result) UnmatchedMocks() []UnmatchedMock
```
## Decoding the response [#decoding-the-response]
`JSON` decodes the response body into a value, for further checks in plain Go:
```go
result := apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
var user struct {
ID string `json:"id"`
Name string `json:"name"`
}
result.JSON(&user)
if user.Name != "jon" {
t.Errorf("unexpected user %+v", user)
}
```
`JSON` reads the body, so call it once per result. It panics if the body cannot be read or is not valid JSON, which fails the test.
## Chaining requests [#chaining-requests]
A result can feed the next request, for example to fetch a resource using the ID returned when it was created:
```go
var created struct {
ID string `json:"id"`
}
apitest.Handler(handler).
Post("/user").
JSON(`{"name": "ola"}`).
Expect(t).
Status(http.StatusCreated).
End().
JSON(&created)
apitest.Handler(handler).
Getf("/user/%s", created.ID).
Expect(t).
Status(http.StatusOK).
Bodyf(`{"id": "%s", "name": "ola"}`, created.ID).
End()
```
## The raw response [#the-raw-response]
`Result.Response` is a copy of the response, with a readable body. Use it with your usual assertion library, or to read headers that are only known at run time.
## Unmatched mocks [#unmatched-mocks]
`UnmatchedMocks` lists the mocks that no request matched. See [call counts](/docs/mocks/call-counts#unmatched-mocks).
# Getting started (/docs/getting-started)
### Install [#install]
apitest requires Go 1.23 or later.
```bash
go get github.com/steinfletcher/apitest
```
apitest follows semantic versioning. Releases are published on [GitHub](https://github.com/steinfletcher/apitest/releases).
### Write a handler [#write-a-handler]
Any `http.Handler` or handler function can be tested. This one greets the caller by name.
```go title="hello.go"
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "hello " + r.URL.Query().Get("name"),
})
}
```
### Write a test [#write-a-test]
Pass the handler to apitest, describe the request, then call `Expect(t)` and describe the response you expect. `End()` runs the test.
```go title="hello_test.go"
func TestHello(t *testing.T) {
apitest.New().
HandlerFunc(helloHandler).
Get("/hello").
Query("name", "gopher").
Expect(t).
Status(http.StatusOK).
Header("Content-Type", "application/json").
Body(`{"message": "hello gopher"}`).
End()
}
```
No server is started. apitest calls the handler's `ServeHTTP` method directly with an `httptest.ResponseRecorder`, so tests are fast and need no free ports.
### Run it [#run-it]
```bash
go test ./...
```
## Reading a failure [#reading-a-failure]
apitest checks every expectation and reports all failures, not just the first. Suppose the test above expected `201 Created` and the body `{"message": "hello world"}`. The output names each mismatch:
```text title="go test output"
--- FAIL: TestHello (0.00s)
assert.go:105:
Error Trace: hello_test.go:18
Error: Not equal:
expected: 201
actual : 200
Test: TestHello
Messages: Status code 200 not equal to 201
assert.go:105:
Error Trace: hello_test.go:18
Error: Not equal:
expected: map[string]interface {}{"message":"hello world"}
actual : map[string]interface {}{"message":"hello gopher"}
Diff:
--- Expected
+++ Actual
@@ -1,4 +1,4 @@
(map[string]interface {}) (len=1) {
- (string) (len=7) "message": (string) (len=11) "hello world"
+ (string) (len=7) "message": (string) (len=12) "hello gopher"
}
Test: TestHello
```
The body was compared as JSON, so the diff shows the decoded values rather than raw strings. Key order and whitespace never cause a failure.
## Next steps [#next-steps]
# AI coding agents (/docs/guides/ai-agents)
Coding agents write better apitest tests when they know the library's conventions and its traps. Two things help: the apitest skill, and these docs in Markdown.
## The apitest skill [#the-apitest-skill]
The apitest repository includes an [Agent Skill](https://agentskills.io) that teaches an agent how to write, fix and review apitest tests. It covers:
- The shape of a test, and the rules that prevent the most common mistakes, such as a missing `End()` or a mock without a `Status`.
- How mocks match requests, and why a client with its own `Transport` bypasses them.
- The JSONPath traps, such as numbers decoding as `float64`.
- Debugging failed tests, table-driven tests, shared configuration and reports.
The full API is in a reference file that the agent reads only when it needs it, so the skill stays small until it is used.
### Claude Code [#claude-code]
Add the apitest repository as a plugin marketplace, then install the plugin:
```bash
claude plugin marketplace add steinfletcher/apitest
```
```bash
claude plugin install apitest@apitest
```
Inside a session, `/plugin marketplace add steinfletcher/apitest` followed by `/plugin install apitest@apitest` does the same.
Claude uses the skill on its own whenever a task involves apitest, and `/apitest:apitest` runs it directly. To fetch the latest version of the skill, run:
```bash
claude plugin update apitest@apitest
```
### Other agents [#other-agents]
The skill follows the open Agent Skills format, so agents that support it can use the folder as it is. Copy `skills/apitest` from the repository into the agent's skills directory. For a single project in Claude Code, that is `.claude/skills/apitest`:
```bash
git clone --depth 1 https://github.com/steinfletcher/apitest.git /tmp/apitest
```
```bash
cp -r /tmp/apitest/skills/apitest .claude/skills/apitest
```
## These docs as Markdown [#these-docs-as-markdown]
Every page of this site is also available as Markdown, with the code samples included:
| URL | Contents |
| --- | --- |
| [`/llms.txt`](/llms.txt) | An index of every page, with links. |
| [`/llms-full.txt`](/llms-full.txt) | The whole documentation in one file. |
| Any docs URL with `.md` added | One page, for example [`/docs/mocks/matching.md`](/docs/mocks/matching.md). |
Each page also has a **Copy Markdown** button below its title, and an **Open** menu that sends the page to ChatGPT, Claude, Cursor or Scira AI.
# Debugging (/docs/guides/debugging)
## Debug [#debug]
`Debug` prints the wire representation of the request into the handler, every mocked call, and the final response:
```go
apitest.New().
Debug().
Mocks(getUserMock, getPreferencesMock).
Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
```text title="go test -v output"
----------> inbound http request
GET /user/1234 HTTP/1.1
Host: sut
----------> request to mock
GET /api/user/1234 HTTP/1.1
Host: users
User-Agent: Go-http-client/1.1
Authorization: Bearer s3cr3t
Accept-Encoding: gzip
<---------- response from mock
HTTP/1.1 200 OK
Content-Length: 29
Content-Type: application/json
{"id": "1234", "name": "jon"}
----------> request to mock
GET /api/preferences/1234 HTTP/1.1
Host: preferences
User-Agent: Go-http-client/1.1
Authorization: Bearer s3cr3t
Accept-Encoding: gzip
<---------- response from mock
HTTP/1.1 200 OK
Content-Length: 23
Content-Type: application/json
{"is_contactable":true}
<---------- final response
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
{"id":"1234","name":"jon","is_contactable":true}
Duration: 985.416µs
```
When a call matches no mock, the output lists why each mock was rejected. See [debugging a mismatch](/docs/mocks/matching#debugging-a-mismatch).
The output goes to standard output. `go test` shows it when the package fails, or always with `-v`.
## Observe [#observe]
`Observe` registers functions that receive the final response, the request, and the test once it has run. It gives programmatic access to the data `Debug` prints:
```go
apitest.New().
Observe(func(res *http.Response, req *http.Request, a *apitest.APITest) {
t.Logf("%s %s -> %d", req.Method, req.URL, res.StatusCode)
observed = true
}).
Mocks(getUserMock, getPreferencesMock).
Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
Observers can make their own checks, for example on timing:
```go
// fail any request that takes longer than 100ms end to end
started := time.Now()
apitest.New().
Observe(func(res *http.Response, req *http.Request, a *apitest.APITest) {
if elapsed := time.Since(started); elapsed > 100*time.Millisecond {
t.Errorf("%s %s took %s", req.Method, req.URL.Path, elapsed)
}
}).
Handler(handler).
Get("/health").
Expect(t).
Status(http.StatusOK).
End()
```
`Observe` replaces any observers registered earlier on the test, and accepts several functions in one call. To observe mocked calls, use [`ObserveMocks`](/docs/mocks/observing).
## Other tools [#other-tools]
- [Sequence diagrams](/docs/reports/sequence-diagrams) give a visual record of a test, which is easier to scan than wire dumps.
- `Result.Response` holds the final response for inspection after `End()`. See [the result](/docs/assertions/result).
# End-to-end tests (/docs/guides/end-to-end)
By default apitest calls the handler in process. `EnableNetworking` sends the request over the network instead, so the same builders can test a server running locally, in a container or in a staging environment.
```go
client := &http.Client{Timeout: 5 * time.Second}
apitest.New().
EnableNetworking(client).
Get(baseURL + "/health").
Expect(t).
Status(http.StatusOK).
End()
```
- The URL must be absolute, including the scheme and host.
- Pass a client to control timeouts, TLS and redirects. Without one, `http.DefaultClient` is used.
- No handler is needed. A test without a handler and without networking fails.
## Sessions [#sessions]
Each apitest test sends one request. A client with a cookie jar keeps cookies between tests, which models a browser session:
```go
func TestLoginFlow(t *testing.T) {
srv := httptest.NewServer(newSessionApp())
defer srv.Close()
// the cookie jar carries the session cookie between requests
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
apitest.New().
EnableNetworking(client).
Post(srv.URL + "/login").
Expect(t).
Status(http.StatusNoContent).
CookiePresent("session").
End()
apitest.New().
EnableNetworking(client).
Get(srv.URL + "/me").
Expect(t).
Status(http.StatusOK).
Body(`{"name": "jon"}`).
End()
}
```
## Mocks and networking [#mocks-and-networking]
Mocks replace a transport inside the test process. They cannot answer calls made by a server running in another process, so end-to-end tests run against real dependencies or the test doubles of that environment.
Keep mocks and networking apart in the same test. Mocks replace the transport of `http.DefaultClient`, or of the client given to [`HttpClient`](/docs/mocks/http-clients), so when the networking client is one of those, the request to the server is matched against the mocks instead of being sent.
## httptest.Server [#httptestserver]
`httptest.NewServer` starts a real server on a free local port, which is useful to test behaviour that only happens over a real connection, such as TLS, HTTP/2 or connection handling. Most tests do not need it, because calling the handler in process is faster and covers the application logic.
# Web frameworks (/docs/guides/frameworks)
apitest accepts any `http.Handler`. The routers of most Go web frameworks implement it, so you pass the router to `Handler`, exactly as you would to `http.ListenAndServe`. Testing through the router also exercises your routes, path parameters and middleware.
Go 1.22 and later support methods and path parameters in `http.ServeMux` patterns:
```go
type User struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
IsContactable bool `json:"is_contactable"`
}
func newUserAPI() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /user/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var user User
if err := getJSON(r.Context(), "http://users/api/user/"+id, &user); err != nil {
writeError(w, err)
return
}
var prefs struct {
IsContactable bool `json:"is_contactable"`
}
if err := getJSON(r.Context(), "http://preferences/api/preferences/"+id, &prefs); err != nil {
writeError(w, err)
return
}
user.IsContactable = prefs.IsContactable
writeJSON(w, http.StatusOK, user)
})
mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil || user.Name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
var created User
if err := postJSON(r.Context(), "http://users/api/user", user, &created); err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusCreated, created)
})
return mux
}
```
```go
apitest.Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
```go
func newChiRouter() http.Handler {
r := chi.NewRouter()
r.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id": "` + chi.URLParam(r, "id") + `"}`))
})
return r
}
func TestChi(t *testing.T) {
apitest.Handler(newChiRouter()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234"}`).
End()
}
```
`*gin.Engine` implements `http.Handler`. `gin.TestMode` silences gin's debug logging.
```go
func newGinEngine() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/user/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"id": c.Param("id")})
})
return r
}
func TestGin(t *testing.T) {
apitest.Handler(newGinEngine()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234"}`).
End()
}
```
`*echo.Echo` implements `http.Handler`.
```go
func newEcho() *echo.Echo {
e := echo.New()
e.GET("/user/:id", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"id": c.Param("id")})
})
return e
}
func TestEcho(t *testing.T) {
apitest.Handler(newEcho()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234"}`).
End()
}
```
## Other frameworks [#other-frameworks]
| Framework | How to test it |
| --- | --- |
| [gorilla/mux](https://github.com/steinfletcher/apitest/tree/master/examples/gorilla) | `*mux.Router` implements `http.Handler`. |
| [httprouter](https://github.com/steinfletcher/apitest/tree/master/examples/httprouter) | `*httprouter.Router` implements `http.Handler`. |
| [iris](https://github.com/steinfletcher/apitest/tree/master/examples/iris) | Call `app.Build()`, then pass `app.Router`. |
| [fiber](https://github.com/steinfletcher/apitest/tree/master/examples/fiber) | fiber is built on fasthttp, not `net/http`. Adapt it with a handler function that calls `app.Test(r)` and copies the response, as in the linked example. |
| [GraphQL with gqlgen](https://github.com/steinfletcher/apitest/tree/master/examples/graphql) | The gqlgen server is an `http.Handler`. |
| [WebSockets](https://github.com/steinfletcher/apitest/tree/master/examples/websockets) | Test the upgrade handshake and HTTP endpoints of a websocket handler. |
## Middleware [#middleware]
Pass the handler with its middleware applied to test the stack your users hit, or the bare handler to test it in isolation. When a handler expects values that middleware adds to the request context, such as an authenticated user, set them with [`WithContext`](/docs/requests/context).
# Table-driven tests (/docs/guides/table-driven-tests)
## Tables of scenarios [#tables-of-scenarios]
Each case in the table describes the dependencies and the expected response. `t.Run` gives every case its own name in the output:
```go
func TestGetUser_Table(t *testing.T) {
tests := []struct {
name string
mocks []*apitest.Mock
wantStatus int
wantBody string
}{
{
name: "found",
mocks: []*apitest.Mock{getUserMock, getPreferencesMock},
wantStatus: http.StatusOK,
wantBody: `{"id": "1234", "name": "jon", "is_contactable": true}`,
},
{
name: "user not found",
mocks: []*apitest.Mock{
apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusNotFound).
End(),
},
wantStatus: http.StatusNotFound,
wantBody: `{"error": "user not found"}`,
},
{
name: "preferences unavailable",
mocks: []*apitest.Mock{
getUserMock,
apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Status(http.StatusServiceUnavailable).
End(),
},
wantStatus: http.StatusBadGateway,
wantBody: `{"error": "upstream error"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apitest.New(tt.name).
Mocks(tt.mocks...).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(tt.wantStatus).
Body(tt.wantBody).
End()
})
}
}
```
Passing the case name to `apitest.New` also names its [sequence diagram](/docs/reports/sequence-diagrams), when reporting is on.
Mocks replace `http.DefaultTransport` for the whole process. Only call `t.Parallel()` in mocked subtests when each one uses its own client. See [parallel tests](/docs/mocks/http-clients#parallel-tests).
## Shared configuration [#shared-configuration]
Every configuration method returns the `*apitest.APITest`, so a helper can set up the handler, mocks, reporting and anything else the tests have in common:
```go
apiTest := func(name string) *apitest.APITest {
return apitest.New(name).
Handler(newUserAPI()).
Mocks(getUserMock, getPreferencesMock)
}
apiTest("gets a user").
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
Tests then add only what is specific to them. A package level helper works the same way, as in [reporting every test](/docs/reports/sequence-diagrams#reporting-every-test).
# Test frameworks (/docs/guides/test-frameworks)
## TestingT [#testingt]
`Expect` accepts an `apitest.TestingT`, the subset of `*testing.T` that apitest uses:
```go title="apitest.TestingT"
type TestingT interface {
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
}
```
Anything that implements these three methods can run apitest tests. This one records failures instead of failing, which is how you test your own helpers and assertions:
```go
// recordingT collects failures instead of failing the test. Anything with
// Errorf, Fatal and Fatalf satisfies apitest.TestingT.
type recordingT struct {
errors []string
}
func (r *recordingT) Errorf(format string, args ...any) {
r.errors = append(r.errors, fmt.Sprintf(format, args...))
}
func (r *recordingT) Fatal(args ...any) { r.errors = append(r.errors, fmt.Sprint(args...)) }
func (r *recordingT) Fatalf(format string, args ...any) {
r.errors = append(r.errors, fmt.Sprintf(format, args...))
}
func TestHandler_RejectsAnonymousUsers(t *testing.T) {
rt := &recordingT{}
apitest.Handler(newSessionApp()).
Get("/me").
Expect(rt).
Status(http.StatusOK).
End()
if len(rt.errors) == 0 {
t.Fatal("expected the status assertion to fail")
}
}
```
## Ginkgo [#ginkgo]
`GinkgoT()` returns a value that satisfies `TestingT` and reports failures to Ginkgo:
```go title="user_test.go"
var _ = Describe("User API", func() {
var (
t GinkgoTInterface
handler http.Handler
)
BeforeEach(func() {
t = GinkgoT()
handler = newRouter()
})
It("gets a user", func() {
apitest.Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
Cookies(apitest.NewCookie("visited").Value("true").Path("/")).
End()
})
})
```
The suite is bootstrapped in the usual way:
```go title="ginkgo_suite_test.go"
package ginkgo_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestUserAPI(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "User API")
}
```
The apitest repository has a complete [Ginkgo example](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo).
## Verifiers [#verifiers]
apitest makes its assertions through a `Verifier`. The default, `apitest.DefaultVerifier`, is based on code from testify and reports failures with `t.Errorf`.
```go title="apitest.Verifier"
type Verifier interface {
Equal(t TestingT, expected, actual any, msgAndArgs ...any) bool
True(t TestingT, value bool, msgAndArgs ...any) bool
JSONEq(t TestingT, expected string, actual string, msgAndArgs ...any) bool
Fail(t TestingT, failureMessage string, msgAndArgs ...any) bool
NoError(t TestingT, err error, msgAndArgs ...any) bool
}
```
`Verifier` replaces it for one test. `apitest.NoopVerifier` makes no assertions at all, which is useful to capture a response, generate a report, or explore an API without failing:
```go
// records the interaction without failing the test
result := apitest.New().
Verifier(apitest.NoopVerifier{}).
Handler(handler).
Get("/brew").
Expect(t).
Status(http.StatusOK).
End()
t.Logf("status was %d", result.Response.StatusCode)
```
Implement `Verifier` to route assertions through another library, or to change how failures are reported. The `apitest/mocks` package has `MockVerifier`, whose methods can each be replaced with a function.
# Handler timeouts (/docs/guides/timeouts)
## Server timeouts [#server-timeouts]
Production servers usually cap how long a handler may run, with `http.TimeoutHandler` or the server's `WriteTimeout`. Wrap the handler in `http.TimeoutHandler` in the test to check the response clients get when a request runs too long:
```go
func TestSlowEndpoint_TimesOut(t *testing.T) {
handler := http.TimeoutHandler(slowHandler, 50*time.Millisecond, "request timed out")
apitest.Handler(handler).
Get("/reports/annual").
Expect(t).
Status(http.StatusServiceUnavailable).
Body("request timed out").
End()
}
```
`TimeoutHandler` responds `503 Service Unavailable` with the message you give it, and cancels the request context, so a well behaved handler stops its work.
## Deadlines from the client [#deadlines-from-the-client]
To check that a handler honours a deadline set by its caller, pass a context with a deadline using [`WithContext`](/docs/requests/context).
## Slow and failing dependencies [#slow-and-failing-dependencies]
To check how a handler copes when the services it calls are slow or time out, use mock [timeouts and delays](/docs/mocks/failures-and-latency).
# Introduction (/docs)
apitest tests a Go HTTP service the way its clients use it. You build a request, send it to the handler, and assert on the response. Calls the handler makes to other services can be answered by mocks, and every test can render a sequence diagram of what happened.
```go title="hello_test.go"
func TestHello(t *testing.T) {
apitest.New().
HandlerFunc(helloHandler).
Get("/hello").
Query("name", "gopher").
Expect(t).
Status(http.StatusOK).
Header("Content-Type", "application/json").
Body(`{"message": "hello gopher"}`).
End()
}
```
## Why apitest [#why-apitest]
In a behavioural test the internal structure of the application is unknown to the test. Data goes in, and the outputs are expected to meet certain conditions. Tests written this way survive refactoring, document how the API behaves, and catch the bugs users would actually see.
apitest keeps those tests short:
- **Fluent builders** describe the request and the expected response in one readable chain.
- **Any `http.Handler`** works, including routers from chi, gin, echo and gorilla. A running server can be tested too.
- **Declarative mocks** answer the outbound HTTP calls your handler makes, with matchers for every part of the request.
- **Sequence diagrams** show each test's request, mocked calls, database queries and response.
- **No third party dependencies.** apitest integrates with `testing`, Ginkgo and your own assertion libraries.
The API is stable. The library is maintained, issues are addressed and feature requests are considered.
## Where to next [#where-to-next]
The code samples are included from a Go module that runs against the latest apitest release, so they compile and pass.
# Call counts (/docs/mocks/call-counts)
By default a mock answers one call. A second matching call skips it and moves on to the next mock, and fails if there is none.
## Times and AnyTimes [#times-and-anytimes]
```go
// must be called exactly twice
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Times(2).
End()
// may be called any number of times, including none
healthCheck := apitest.NewMock().
Get("http://users/health").
RespondWith().
Status(http.StatusOK).
AnyTimes().
End()
apitest.New().
Mocks(getUser, healthCheck).
Handler(handler).
Get("/").
Expect(t).
Status(http.StatusOK).
End()
```
| Method | Behaviour |
| --- | --- |
| `Times(n)` | Answers `n` calls, and fails the test if fewer are made. A further call does not match. |
| `AnyTimes()` | Answers any number of calls, including none. It takes precedence over `Times`. |
A mock without `Times` that is never called does not fail the test. It is reported as unmatched instead.
## Unmatched mocks [#unmatched-mocks]
`End()` returns a [result](/docs/assertions/result) whose `UnmatchedMocks` method lists the mocks that no request matched:
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End()
getPreferences := apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"is_contactable": true}`).
End()
audit := apitest.NewMock().
Post("http://audit/api/events").
RespondWith().
Status(http.StatusAccepted).
End()
result := apitest.New().
Mocks(getUser, getPreferences, audit).
Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
for _, unused := range result.UnmatchedMocks() {
t.Logf("mock was not called: %s", unused.URL.String())
}
```
An unused mock often means the test sets up a dependency the code no longer calls. Logging them, as above, keeps mock setup honest without making every test strict. To make a call mandatory, use `Times(1)`.
# Custom matchers (/docs/mocks/custom-matchers)
`AddMatcher` adds a matcher alongside the built-in ones. It receives the outbound request and the mock's request specification, and returns an error when the request should not match. The error message is shown in [debug output](/docs/mocks/matching#debugging-a-mismatch).
```go
getPreferences := apitest.NewMock().
Get("/api/preferences/1234").
AddMatcher(func(r *http.Request, mr *apitest.MockRequest) error {
if r.URL.Scheme != "https" {
return errors.New("expected an https request")
}
return nil
}).
AddMatcher(hasRequestID).
RespondWith().
Status(http.StatusOK).
End()
```
```go title="apitest.Matcher"
type Matcher func(*http.Request, *apitest.MockRequest) error
```
Matchers are values, so they can be named and shared:
```go
// hasRequestID matches requests that carry a UUID request id.
var hasRequestID apitest.Matcher = func(r *http.Request, _ *apitest.MockRequest) error {
id := r.Header.Get("X-Request-Id")
if len(id) != 36 || strings.Count(id, "-") != 4 {
return errors.New("expected X-Request-Id to be a UUID")
}
return nil
}
```
A matcher that reads `r.Body` must replace it afterwards, for example with `io.NopCloser(bytes.NewReader(b))`, so later matchers, the other mocks and debug output can still read it.
## JSONPath matchers [#jsonpath-matchers]
The `mocks` package of [apitest-jsonpath](/docs/assertions/jsonpath) provides matchers for JSON request bodies. They match part of the body, which suits payloads that contain IDs or timestamps.
```go
import "github.com/steinfletcher/apitest-jsonpath/mocks"
```
```go
createAdmin := apitest.NewMock().
Post("http://users/api/user").
AddMatcher(mocks.Equal(`$.name`, "jon")).
AddMatcher(mocks.Contains(`$.roles`, "admin")).
AddMatcher(mocks.Len(`$.roles`, 2)).
RespondWith().
Status(http.StatusCreated).
End()
```
| Matcher | Matches when |
| --- | --- |
| `mocks.Equal(expr, v)` | The expression selects a value equal to `v`. |
| `mocks.NotEqual(expr, v)` | The expression selects a value not equal to `v`. |
| `mocks.Contains(expr, v)` | The expression selects an array that contains `v`. |
| `mocks.Len(expr, n)` | The selected value has length `n`. |
| `mocks.GreaterThan(expr, n)` | The selected value has length of at least `n`. |
As with the assertions, JSON numbers decode as `float64`.
# Defining mocks (/docs/mocks/defining-mocks)
`apitest.NewMock()` starts a mock. A method such as `Get` sets the request to match, `RespondWith()` switches to the response, and `End()` finishes the mock:
```go
createUser := apitest.NewMock().
Post("https://users/api/user").
Header("Authorization", "Bearer .+").
Query("notify", "true").
JSON(`{"name": "jon"}`).
RespondWith().
Status(http.StatusCreated).
JSON(`{"id": "1234", "name": "jon"}`).
End()
```
Pass mocks to the test with `Mocks`, which takes any number of them:
```go
apitest.New().
Mocks(getUser, getPreferences).
Handler(handler).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
Calling `Mocks` again replaces the earlier list, so pass every mock in one call.
## The request [#the-request]
The verb methods set the method and URL to match:
| Method | Matches |
| --- | --- |
| `Get`, `Post`, `Put`, `Patch`, `Delete`, `Head` | The method and URL. |
| `Getf`, `Postf`, `Putf`, `Patchf`, `Deletef` | The method and a URL formatted with `fmt.Sprintf`. |
| `Method(method)` | Any method, such as `OPTIONS`. Set the URL with `URL` or `URLf`, or leave it out to match any URL. |
The URL's path is matched as a regular expression, and its scheme and host are matched when present. See [how matching works](/docs/mocks/matching).
```go
preflight := apitest.NewMock().
Method(http.MethodOptions).
URL("http://users/api/user").
RespondWith().
Status(http.StatusNoContent).
End()
```
The request side then accepts [matchers](/docs/mocks/request-matchers) for headers, query parameters, the body, form data, cookies and basic auth, plus [custom matchers](/docs/mocks/custom-matchers).
## The response [#the-response]
After `RespondWith()`, set what the mock returns. See [responses](/docs/mocks/responses).
A mock without `Status` responds with status code `0`, which is rarely what the code under test expects.
## Sharing mocks [#sharing-mocks]
Mocks are values, so they can be declared once as package variables and reused by many tests:
```go title="mocks_test.go"
var getUserMock = apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End()
var getPreferencesMock = apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Status(http.StatusOK).
JSON(map[string]any{"is_contactable": true}).
End()
```
A test copies the mocks it receives before counting calls, so the call count of a shared mock starts fresh in every test.
# Failures and latency (/docs/mocks/failures-and-latency)
## Timeouts [#timeouts]
`Timeout` makes the mocked call fail with a timeout error. The client receives an error whose `Timeout()` method returns `true`, just as it would if the dependency stopped responding:
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End()
preferencesTimeout := apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Timeout().
End()
apitest.New().
Mocks(getUser, preferencesTimeout).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusGatewayTimeout).
End()
```
This is the reliable way to test timeout handling, because nothing has to wait.
## Slow responses [#slow-responses]
`FixedDelay` waits the given number of milliseconds before responding. Delays are off by default, so a test opts in with `EnableMockResponseDelay`:
```go
slowUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
FixedDelay(250).
End()
apitest.New().
EnableMockResponseDelay().
Mocks(slowUser, getPreferencesMock).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
Keeping delays off by default means shared mocks can carry realistic delays without slowing down every test that uses them. `FixedDelay` has no effect when `Timeout` is set.
The delay is a sleep before the response is returned. It does not observe the request context or the client's `Timeout`, so a client with a 50 millisecond timeout still receives a response from a mock with a 250 millisecond delay. Use `Timeout` to test timeout handling.
## Error responses [#error-responses]
Error statuses need nothing special. Respond with the status and body the dependency would send:
```go
userNotFound := apitest.NewMock().
Get("http://users/api/user/9999").
RespondWith().
Status(http.StatusNotFound).
End()
apitest.New().
Mocks(userNotFound).
Handler(newUserAPI()).
Get("/user/9999").
Expect(t).
Status(http.StatusNotFound).
Body(`{"error": "user not found"}`).
End()
```
# HTTP clients (/docs/mocks/http-clients)
By default, mocks replace `http.DefaultTransport`. That covers `http.Get`, `http.DefaultClient` and every `http.Client` created without a `Transport`.
A client with its own transport, such as one configured with custom TLS or connection pooling settings, bypasses `http.DefaultTransport`. Pass it to the test with `HttpClient` so its transport is replaced instead:
```go
type UserClient struct {
http *http.Client
}
func (c *UserClient) Handler() http.Handler {
return forwardWith(c.http, func() *http.Request {
return newRequest(http.MethodGet, "http://users/api/user/1234", "")
})
}
func TestMocks_CustomHTTPClient(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
app := &UserClient{http: client}
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
End()
apitest.New().
HttpClient(client).
Mocks(getUser).
Handler(app.Handler()).
Get("/").
Expect(t).
Status(http.StatusOK).
End()
}
```
The client's original transport is restored when the test ends.
Most applications create their clients at start up. Make the client injectable, as a field or a constructor argument, so the test can pass the same client to the application and to apitest.
## Parallel tests [#parallel-tests]
`http.DefaultTransport` is shared by the whole process. Two tests that replace it at the same time interfere with each other, so tests that mock the default transport must not call `t.Parallel()`.
To run mocked tests in parallel, give each test its own client and pass it with `HttpClient`:
```go
for _, id := range []string{"1", "2", "3"} {
t.Run(id, func(t *testing.T) {
t.Parallel()
// each subtest has its own client, so the mocks do not
// replace the process-wide http.DefaultTransport
client := &http.Client{}
handler := forwardWith(client, func() *http.Request {
return newRequest(http.MethodGet, "http://users/api/user/"+id, "")
})
getUser := apitest.NewMock().
Get("http://users/api/user/" + id).
RespondWith().
Status(http.StatusOK).
End()
apitest.New().
HttpClient(client).
Mocks(getUser).
Handler(handler).
Get("/").
Expect(t).
Status(http.StatusOK).
End()
})
}
```
## Clients that are not net/http [#clients-that-are-not-nethttp]
Mocks work at the `http.RoundTripper` level. SDKs built on `net/http`, such as most cloud and API clients, can be mocked if they let you supply the `*http.Client`. Clients that use a different networking stack, such as gRPC, cannot be mocked this way.
# Mocking HTTP calls (/docs/mocks)
Most services call other services. Calling the real ones from a test makes it slow, flaky and dependent on data you do not control. apitest mocks answer those calls instead, so each test decides exactly what its dependencies return.
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End()
getPreferences := apitest.NewMock().
Get("http://preferences/api/preferences/1234").
RespondWith().
Status(http.StatusOK).
JSON(map[string]any{"is_contactable": false}).
End()
apitest.New().
Mocks(getUser, getPreferences).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon", "is_contactable": false}`).
End()
```
A mock describes the request to match and the response to return. When the handler under test calls `http://users/api/user/1234`, the first mock answers it. Nothing leaves the process.
## How mocks work [#how-mocks-work]
Go's HTTP clients send requests through a `http.RoundTripper`, called a transport. When a test has mocks, apitest swaps the transport for its own for the duration of the test:
1. Before the request is sent, `http.DefaultTransport` is replaced. With [`HttpClient`](/docs/mocks/http-clients), the transport of that client is replaced instead.
2. Each outbound request is compared with the mocks, in order. The first mock whose [matchers](/docs/mocks/matching) all pass builds the response.
3. A request that matches no mock fails with an error that explains why each mock was rejected.
4. When the test ends, the original transport is restored.
`http.Get`, `http.Post`, `http.DefaultClient` and any `http.Client` without its own `Transport` use `http.DefaultTransport`, so they are mocked with no changes to your code.
While mocks are active, every request through the replaced transport must match a mock. There is no fall through to the real network.
## Testing error paths [#testing-error-paths]
Mocks make failure scenarios easy to set up, such as a dependency returning `404`:
```go
userNotFound := apitest.NewMock().
Get("http://users/api/user/9999").
RespondWith().
Status(http.StatusNotFound).
End()
apitest.New().
Mocks(userNotFound).
Handler(newUserAPI()).
Get("/user/9999").
Expect(t).
Status(http.StatusNotFound).
Body(`{"error": "user not found"}`).
End()
```
[Failures and latency](/docs/mocks/failures-and-latency) covers timeouts and slow responses.
## Mocks complement integration tests [#mocks-complement-integration-tests]
Mocks keep the feedback loop short while you develop, but they only know what you tell them. Keep some tests that exercise the real dependencies, for example [end-to-end tests](/docs/guides/end-to-end) against a staging environment, to catch changes in their behaviour.
## In this section [#in-this-section]
# How matching works (/docs/mocks/matching)
## Order [#order]
For each outbound request, mocks are tried in the order they were passed to `Mocks`. The first mock whose matchers all pass answers the request. By default a mock answers one request and is then skipped, so mocks for the same URL answer successive calls in turn:
```go
// the first call fails, the next two succeed
unavailable := apitest.NewMock().
Get("http://flaky/api/status").
RespondWith().
Status(http.StatusServiceUnavailable).
End()
available := apitest.NewMock().
Get("http://flaky/api/status").
RespondWith().
Status(http.StatusOK).
Times(2).
End()
apitest.New().
Mocks(unavailable, available).
Handler(handler).
Get("/").
Expect(t).
Body("503,200,200").
End()
```
[Call counts](/docs/mocks/call-counts) explains `Times` and `AnyTimes`.
## The rules [#the-rules]
| Part | Rule |
| --- | --- |
| Path | Equal to the mock path, or matched by it as a regular expression. Ignored when the mock has no URL. |
| Host | Equal to the mock host. Ignored when the mock URL has no host. |
| Scheme | Equal to the mock scheme. Ignored when the mock URL has no scheme. |
| Method | Equal to the mock method. |
| Headers, query parameters, form data | Each value is a regular expression matched against the received values. |
| Body | Equal to the mock body as a string, or equivalent JSON. `BodyRegexp` matches a regular expression. |
| Cookies, basic auth | Equal to the mock's values. |
| Custom matchers | Return `nil`. |
Every matcher must pass. A matcher whose part of the mock was not set, such as headers on a mock without `Header`, always passes.
## Paths are regular expressions [#paths-are-regular-expressions]
The mock path is a regular expression, and it is not anchored. `Get("http://users/api/user/\\d+")` matches `/api/user/1234`, and also `/api/user/1234/avatar`:
```go
// matches /api/user/1234 and /api/user/1234/avatar: the expression is not anchored
anyUserPath := apitest.NewMock().
Get(`http://users/api/user/\d+`).
RespondWith().
Status(http.StatusOK).
End()
// $ anchors the end, so /api/user/1234/avatar no longer matches
userOnly := apitest.NewMock().
Get(`http://users/api/user/\d+$`).
RespondWith().
Status(http.StatusOK).
End()
// leave out the scheme and host to anchor the start with ^ as well
anchored := apitest.NewMock().
Get(`^/api/user/\d+$`).
RespondWith().
Status(http.StatusOK).
End()
```
Regular expression characters in a path, such as `.`, `+` and `?`, also need escaping when you want them matched literally.
## Debugging a mismatch [#debugging-a-mismatch]
When no mock matches, the client receives an error, and the handler usually responds with an error status. Enable [`Debug`](/docs/guides/debugging) to see why each mock was rejected. In this run the users mock has a typo in its path, and the preferences mock expects a different token:
```text title="Debug output"
----------> inbound http request
GET /user/1234 HTTP/1.1
Host: sut
failed to match mocks. Errors: received request did not match any mocks
Mock 1 mismatches:
• received path /api/user/1234 did not match mock path /api/user/12345
Mock 2 mismatches:
• received path /api/user/1234 did not match mock path /api/preferences/1234
• received host users did not match mock host preferences
• not all of received headers map[Authorization:[Bearer s3cr3t]] matched expected mock headers map[Authorization:[Bearer token]]
----------> request to mock
GET /api/user/1234 HTTP/1.1
Host: users
User-Agent: Go-http-client/1.1
Authorization: Bearer s3cr3t
Accept-Encoding: gzip
<---------- response from mock
<---------- final response
HTTP/1.1 502 Bad Gateway
Content-Type: application/json
{"error":"upstream error"}
```
Mocks that have already answered their calls are skipped, so they do not appear in the list.
### In a sequence diagram [#in-a-sequence-diagram]
A [sequence diagram](/docs/reports/sequence-diagrams) shows the same failure at a glance. A call that matched no mock gets no response, so the report marks its arrow with a ✕ and labels the message **no response**:
```go
// the mock is for user 9999, so the call for user 1234 matches no mock
wrongUser := apitest.NewMock().
Get("http://users/api/user/9999").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "9999", "name": "ola"}`).
End()
apitest.New("reports a call that matched no mock").
Report(apitest.SequenceDiagram()).
Mocks(wrongUser).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusBadGateway).
End()
```
[`ObserveMocks`](/docs/mocks/observing) reports every mocked call programmatically, including the ones that did not match.
# Observing mocks (/docs/mocks/observing)
`ObserveMocks` registers a function that is called after every outbound call that reaches the mock transport:
```go
apitest.New().
ObserveMocks(func(res *http.Response, req *http.Request, a *apitest.APITest) {
if res == nil {
t.Logf("%s %s did not match a mock", req.Method, req.URL)
return
}
t.Logf("%s %s -> %d", req.Method, req.URL, res.StatusCode)
observed.Add(1)
}).
Mocks(getUserMock, getPreferencesMock).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
```go title="apitest.Observe"
type Observe func(*http.Response, *http.Request, *apitest.APITest)
```
The response is `nil` when the call did not match a mock, or when the mock was set to [time out](/docs/mocks/failures-and-latency#timeouts). `ObserveMocks` can be called several times to register several observers.
Observers suit cross-cutting checks, such as asserting that every outbound call carries a tracing header, or collecting calls for a custom report. [Sequence diagrams](/docs/reports/sequence-diagrams) are built on the same hook.
To observe the final response of the handler instead, use [`Observe`](/docs/guides/debugging#observe).
# Request matchers (/docs/mocks/request-matchers)
Matchers narrow down which requests a mock answers. They are set between the verb method and `RespondWith()`, and every one must pass for the mock to match.
## Headers [#headers]
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
Header("Authorization", "^Bearer .+$").
Headers(map[string]string{"Accept": "application/json"}).
HeaderPresent("X-Request-Id").
HeaderNotPresent("X-Debug").
RespondWith().
Status(http.StatusOK).
End()
```
| Method | Matches when |
| --- | --- |
| `Header(key, pattern)` | A value of the header matches the regular expression. |
| `Headers(map[string]string)` | Each entry matches as it would with `Header`. |
| `HeaderPresent(key)` | The header exists, whatever its value. |
| `HeaderNotPresent(key)` | The header does not exist. |
## Query parameters [#query-parameters]
```go
search := apitest.NewMock().
Get("http://search/api/users").
Query("q", "jon").
Query("page", `^\d+$`).
QueryParams(map[string]string{"sort": "asc"}).
QueryCollection(map[string][]string{"tag": {"a", "b"}}).
QueryPresent("page").
QueryNotPresent("debug").
RespondWith().
Status(http.StatusOK).
End()
```
| Method | Matches when |
| --- | --- |
| `Query(key, pattern)` | A value of the parameter matches the regular expression. |
| `QueryParams(map[string]string)` | Each entry matches as it would with `Query`. |
| `QueryCollection(map[string][]string)` | Each listed value matches a value of the parameter. |
| `QueryPresent(key)` | The parameter exists. |
| `QueryNotPresent(key)` | The parameter does not exist. |
Values are regular expressions, so `Query("page", "1")` also matches `page=10`. Anchor the pattern, as in `^1$`, for an exact match.
## Body [#body]
```go
// exact string or equivalent JSON
byBody := apitest.NewMock().
Post("http://users/api/user").
Body(`{"name": "jon", "age": 32}`).
RespondWith().
Status(http.StatusCreated).
End()
// a value marshalled to JSON
byJSON := apitest.NewMock().
Post("http://users/api/user").
JSON(map[string]any{"name": "jon", "age": 32}).
RespondWith().
Status(http.StatusCreated).
End()
// a regular expression
byRegexp := apitest.NewMock().
Post("http://users/api/user").
BodyRegexp(`"name":\s*"jo.+"`).
RespondWith().
Status(http.StatusCreated).
End()
```
| Method | Matches when |
| --- | --- |
| `Body(s)` | The body equals `s`, or both are JSON and decode to equal values. |
| `Bodyf(format, args...)` | As `Body`, with the expected body formatted by `fmt.Sprintf`. |
| `JSON(v)` | As `Body`. Strings and byte slices are used as is, and other values are marshalled. |
| `BodyFromFile(path)` | As `Body`, with the expected body read from a file. |
| `BodyRegexp(pattern)` | The body matches the regular expression. |
```go
createUser := apitest.NewMock().
Post("http://users/api/user").
BodyFromFile("testdata/create-user.json").
RespondWith().
Status(http.StatusCreated).
End()
```
To match part of a JSON body, use the [JSONPath matchers](/docs/mocks/custom-matchers#jsonpath-matchers).
## Form data [#form-data]
```go
token := apitest.NewMock().
Post("http://auth/oauth/token").
FormData("grant_type", "client_credentials").
FormData("scope", "users:.*").
FormDataPresent("client_id").
FormDataNotPresent("client_secret").
RespondWith().
Status(http.StatusOK).
JSON(`{"access_token": "abc", "token_type": "Bearer"}`).
End()
```
| Method | Matches when |
| --- | --- |
| `FormData(key, patterns...)` | Each pattern matches a value of the field in the URL encoded body. |
| `FormDataPresent(key)` | The field exists. |
| `FormDataNotPresent(key)` | The field does not exist. |
## Cookies and basic auth [#cookies-and-basic-auth]
```go
invoices := apitest.NewMock().
Get("http://billing/api/invoices").
Cookie("session", "12345").
CookiePresent("tracking").
CookieNotPresent("legacy").
BasicAuth("billing-svc", "pa55word").
RespondWith().
Status(http.StatusOK).
End()
```
| Method | Matches when |
| --- | --- |
| `Cookie(name, value)` | A cookie with the name has exactly that value. |
| `CookiePresent(name)` | A cookie with the name exists. |
| `CookieNotPresent(name)` | No cookie with the name exists. |
| `BasicAuth(username, password)` | The request carries exactly these basic auth credentials. |
## Methods and URLs [#methods-and-urls]
`Head` matches `HEAD` requests, alongside `Get`, `Post`, `Put`, `Patch` and `Delete`:
```go
logoExists := apitest.NewMock().
Head("http://cdn/assets/logo.png").
RespondWith().
Status(http.StatusOK).
Header("Content-Length", "2048").
End()
```
`Method` matches any other method. Set the URL to match with `URL`, or with `URLf` to format it:
```go
preflight := apitest.NewMock().
Method(http.MethodOptions).
URL("http://users/api/user").
RespondWith().
Status(http.StatusNoContent).
End()
```
A mock started with `Method` and no URL matches every request with that method, whatever its scheme, host and path:
```go
// without a URL, the mock matches every OPTIONS request
anyPreflight := apitest.NewMock().
Method(http.MethodOptions).
RespondWith().
Status(http.StatusNoContent).
AnyTimes().
End()
```
`URL` and `URLf` were added in v1.7.0. On earlier versions, a mock built with `Method` and no URL panics when a request is matched against it.
# Responses (/docs/mocks/responses)
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Header("Cache-Control", "no-store").
Headers(map[string]string{"X-Request-Id": "abc"}).
Cookie("session", "12345").
Cookies(apitest.NewCookie("theme").Value("dark").Path("/")).
Bodyf(`{"id": "%s", "name": "jon"}`, "1234").
End()
```
| Method | Sets |
| --- | --- |
| `Status(code)` | The status code. Defaults to `0`, so always set it. |
| `Header(key, value)` | Adds a header value. |
| `Headers(map[string]string)` | Adds a header value for each entry. |
| `Cookie(name, value)` | Adds a `Set-Cookie` header. |
| `Cookies(...*apitest.Cookie)` | Adds a `Set-Cookie` header per cookie, with any attributes you set. |
| `Body(s)` | The body. |
| `Bodyf(format, args...)` | The body, formatted with `fmt.Sprintf`. |
| `JSON(v)` | The body. Strings and byte slices are used as is, and other values are marshalled. |
| `BodyFromFile(path)` | The body, read from a file. |
## Content type [#content-type]
When the body is not empty and no `Content-Type` header is set, the mock sets one: `application/json` when the body is valid JSON, and `text/plain` otherwise. Set the header yourself for anything else, such as `application/problem+json`.
## Bodies from files [#bodies-from-files]
Recorded responses from the real service make realistic fixtures:
```go
getUser := apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
BodyFromFile("testdata/user.json").
End()
```
## Errors and delays [#errors-and-delays]
The response side also controls how the call behaves: `Timeout` makes it fail with a timeout, `FixedDelay` makes it slow, and `Times` and `AnyTimes` control how many calls the mock answers. See [failures and latency](/docs/mocks/failures-and-latency) and [call counts](/docs/mocks/call-counts).
# Standalone mocks (/docs/mocks/standalone)
Mocks do not need an apitest test. `EndStandalone` installs a mock and returns a function that removes it, which makes apitest a convenient way to test HTTP client code:
```go
// UsersClient is an HTTP client for the users service.
type UsersClient struct {
BaseURL string
}
func (c UsersClient) GetUser(id string) (User, error) {
var user User
err := getJSON(context.Background(), c.BaseURL+"/api/user/"+id, &user)
return user, err
}
func TestUsersClient_GetUser(t *testing.T) {
defer apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
EndStandalone()()
user, err := UsersClient{BaseURL: "http://users"}.GetUser("1234")
if err != nil {
t.Fatal(err)
}
if user.Name != "jon" {
t.Fatalf("unexpected user %+v", user)
}
}
```
`EndStandalone()` returns the reset function, so `defer ...EndStandalone()()` installs the mock now and removes it when the test returns.
## Several mocks [#several-mocks]
`EndStandalone` accepts more mocks as arguments. `NewStandaloneMocks` reads more clearly when there are several:
```go
reset := apitest.NewStandaloneMocks(
apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
End(),
apitest.NewMock().
Get("http://users/api/user/9999").
RespondWith().
Status(http.StatusNotFound).
End(),
).End()
defer reset()
client := UsersClient{BaseURL: "http://users"}
if _, err := client.GetUser("1234"); err != nil {
t.Fatal(err)
}
if _, err := client.GetUser("9999"); !errors.Is(err, errNotFound) {
t.Fatalf("expected not found, got %v", err)
}
```
## Custom clients and debugging [#custom-clients-and-debugging]
`HttpClient` replaces the transport of a specific client, and `Debug` prints each call and the reasons for any mismatch:
```go
cli := &http.Client{Timeout: time.Second}
reset := apitest.NewStandaloneMocks(
apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
End(),
).
HttpClient(cli).
Debug().
End()
defer reset()
res, err := cli.Get("http://users/api/user/1234")
```
On a single mock, the equivalents are `apitest.NewMock().HttpClient(cli)` and `apitest.NewMock().Debug()`.
## Repeated calls [#repeated-calls]
As in a test, a standalone mock answers one call by default. `Times(n)` makes it answer `n` calls, and `AnyTimes()` any number:
```go
defer apitest.NewMock().
Get("http://users/api/user/1234").
RespondWith().
Status(http.StatusOK).
Body(`{"id": "1234", "name": "jon"}`).
Times(2).
EndStandalone()()
client := UsersClient{BaseURL: "http://users"}
for range 2 {
if _, err := client.GetUser("1234"); err != nil {
t.Fatal(err)
}
}
```
Standalone mode does not check call counts, so a mock called fewer times than `Times` asks for does not fail the test. To require every call, run the code inside an apitest test, or count the calls with your own assertions.
Earlier versions ignored `Times` on standalone mocks, so each one answered a single call unless `AnyTimes` was set.
As with mocks in a test, standalone mocks on the default transport affect the whole process, so do not run these tests in parallel unless each uses its own client.
# API overview (/docs/reference/api)
This page lists the whole API of `github.com/steinfletcher/apitest` , the release these docs are tested against, with links to the guides. Signatures and doc comments are on [pkg.go.dev](https://pkg.go.dev/github.com/steinfletcher/apitest).
## Starting a test [#starting-a-test]
| Function | Returns | Description |
| --- | --- | --- |
| `New(name ...string)` | `*APITest` | Starts a test. The optional name appears in reports. |
| `Handler(h http.Handler)` | `*APITest` | Shorthand for `New().Handler(h)`. |
| `HandlerFunc(f http.HandlerFunc)` | `*APITest` | Shorthand for `New().HandlerFunc(f)`. |
## APITest [#apitest]
Configuration methods return `*APITest`. The request methods return `*Request`. See [anatomy of a test](/docs/anatomy).
| Method | Description |
| --- | --- |
| `Handler(http.Handler)`, `HandlerFunc(http.HandlerFunc)` | The handler under test. [Requests](/docs/requests) |
| `Mocks(...*Mock)` | The mocks for this test. Replaces earlier mocks. [Mocks](/docs/mocks) |
| `HttpClient(*http.Client)` | Mock the transport of this client instead of `http.DefaultTransport`. [HTTP clients](/docs/mocks/http-clients) |
| `EnableMockResponseDelay()` | Honour `FixedDelay` on mocks. [Failures and latency](/docs/mocks/failures-and-latency) |
| `ObserveMocks(Observe)` | Called for every mocked call. [Observing mocks](/docs/mocks/observing) |
| `Observe(...Observe)` | Called with the final response. Replaces earlier observers. [Debugging](/docs/guides/debugging#observe) |
| `Debug()` | Print every request and response. [Debugging](/docs/guides/debugging) |
| `Report(ReportFormatter)` | Render the test as a report. [Sequence diagrams](/docs/reports/sequence-diagrams) |
| `Recorder(*Recorder)` | Use this recorder, for custom events. [Custom reports](/docs/reports/custom-reports) |
| `Meta(map[string]any)` | Values passed to reports. [Sequence diagrams](/docs/reports/sequence-diagrams#naming-participants-and-choosing-a-directory) |
| `Intercept(Intercept)` | Change the request before it is sent. [Intercept](/docs/requests/intercept) |
| `EnableNetworking(...*http.Client)` | Send the request over the network. [End-to-end tests](/docs/guides/end-to-end) |
| `Verifier(Verifier)` | Replace the assertion implementation. [Test frameworks](/docs/guides/test-frameworks#verifiers) |
| `UseFS(fs.FS)` | The file system for `MultipartFile`. [Forms](/docs/requests/forms#files-from-memory) |
| `Host(string)` | The request `Host`. Defaults to `sut`. [Requests](/docs/requests#host) |
| `Get`, `Post`, `Put`, `Patch`, `Delete` `(url string)` | Start the request with a method and URL. |
| `Getf`, `Postf`, `Putf`, `Patchf`, `Deletef` `(format string, args ...any)` | As above, with a formatted URL. |
| `Method(string)` | Start the request with a method. Set the URL with `URL`. |
| `HttpRequest(*http.Request)` | Use a ready-made request. [Requests](/docs/requests#using-an-httprequest) |
| `Request()`, `Response()` | The request and expectation builders of the test. |
| `RecorderHook(RecorderHook)` | Deprecated. Use `Recorder`. |
## Request [#request]
Request methods return `*Request`. `Expect(t)` returns `*Response`.
| Method | Description |
| --- | --- |
| `URL(string)`, `URLf(format, args...)` | The URL. |
| `Header(key, value)`, `Headers(map[string]string)` | Add headers. [Headers](/docs/requests/headers) |
| `ContentType(string)` | Set `Content-Type`. |
| `Query(key, value)`, `QueryParams(map[string]string)`, `QueryCollection(map[string][]string)` | Add query parameters. [Query parameters](/docs/requests/query-parameters) |
| `Cookie(name, value)`, `Cookies(...*Cookie)` | Add cookies. [Cookies](/docs/requests/cookies) |
| `Body(string)`, `Bodyf(format, args...)`, `BodyFromFile(path)` | Set the body. [Body](/docs/requests/body) |
| `JSON(any)`, `JSONFromFile(path)` | Set a JSON body and content type. [Body](/docs/requests/body#json) |
| `FormData(name, values...)` | Add URL encoded form fields. [Forms](/docs/requests/forms) |
| `MultipartFormData(name, values...)`, `MultipartFile(name, paths...)` | Add multipart fields and files. [Forms](/docs/requests/forms#multipart-forms) |
| `GraphQLQuery(query, variables...)`, `GraphQLRequest(GraphQLRequestBody)` | Build a GraphQL request. [GraphQL](/docs/requests/graphql) |
| `BasicAuth(username, password)` | Set basic auth credentials. [Authentication](/docs/requests/authentication) |
| `WithContext(context.Context)` | Set the request context. [Context](/docs/requests/context) |
| `Expect(TestingT)` | End the request and start the expectations. |
## Response [#response]
Expectation methods return `*Response`. `End()` returns `Result`.
| Method | Description |
| --- | --- |
| `Status(int)` | The status code. [Assertions](/docs/assertions#status) |
| `Body(string)`, `Bodyf(format, args...)`, `BodyFromFile(path)` | The body, compared as JSON when valid JSON. [Body](/docs/assertions/body) |
| `Header(key, value)`, `Headers(map[string]string)` | Header values. [Headers](/docs/assertions/headers) |
| `HeaderPresent(key)`, `HeaderNotPresent(key)` | Header presence. |
| `Cookie(name, value)`, `Cookies(...*Cookie)` | Cookies. [Cookies](/docs/assertions/cookies) |
| `CookiePresent(name)`, `CookieNotPresent(name)` | Cookie presence. |
| `Assert(func(*http.Response, *http.Request) error)` | A custom assertion. [Custom assertions](/docs/assertions/custom) |
| `End()` | Run the test. |
## Result [#result]
| Member | Description |
| --- | --- |
| `Response *http.Response` | A copy of the final response. [The result](/docs/assertions/result) |
| `JSON(any)` | Decode the response body. |
| `UnmatchedMocks() []UnmatchedMock` | Mocks no request matched. Each has a `URL url.URL`. |
## Mock [#mock]
`NewMock()` returns `*Mock`. The verb methods return `*MockRequest`.
| Method | Description |
| --- | --- |
| `Get`, `Post`, `Put`, `Patch`, `Delete`, `Head` `(url string)` | Match the method and URL. [Defining mocks](/docs/mocks/defining-mocks) |
| `Getf`, `Postf`, `Putf`, `Patchf`, `Deletef` `(format, args...)` | As above, with a formatted URL. |
| `Method(string)` | Match any method. Matches any URL unless `URL` or `URLf` sets one. [Request matchers](/docs/mocks/request-matchers#methods-and-urls) |
| `HttpClient(*http.Client)`, `Debug()` | Options for [standalone mocks](/docs/mocks/standalone). |
| `Matches(*http.Request) []error` | Run the mock's matchers against a request. |
## MockRequest [#mockrequest]
Matchers return `*MockRequest`. `RespondWith()` returns `*MockResponse`. See [request matchers](/docs/mocks/request-matchers).
| Method | Matches |
| --- | --- |
| `URL(url)`, `URLf(format, args...)` | The URL, for a mock started with `Method`. Added in v1.7.0. |
| `Header(key, pattern)`, `Headers(map[string]string)` | Header values, as regular expressions. |
| `HeaderPresent(key)`, `HeaderNotPresent(key)` | Header presence. |
| `Query(key, pattern)`, `QueryParams(map[string]string)`, `QueryCollection(map[string][]string)` | Query values, as regular expressions. |
| `QueryPresent(key)`, `QueryNotPresent(key)` | Query presence. |
| `Body(string)`, `Bodyf(format, args...)`, `BodyFromFile(path)`, `JSON(any)` | The body, exactly or as equivalent JSON. |
| `BodyRegexp(pattern)` | The body, as a regular expression. |
| `FormData(key, patterns...)` | Form values, as regular expressions. |
| `FormDataPresent(key)`, `FormDataNotPresent(key)` | Form field presence. |
| `Cookie(name, value)`, `CookiePresent(name)`, `CookieNotPresent(name)` | Cookies. |
| `BasicAuth(username, password)` | Basic auth credentials. |
| `AddMatcher(Matcher)` | A [custom matcher](/docs/mocks/custom-matchers). |
| `RespondWith()` | End the request and start the response. |
## MockResponse [#mockresponse]
Methods return `*MockResponse`. `End()` returns `*Mock`. See [responses](/docs/mocks/responses).
| Method | Description |
| --- | --- |
| `Status(int)` | The status code. Defaults to `0`. |
| `Header(key, value)`, `Headers(map[string]string)` | Response headers. |
| `Cookie(name, value)`, `Cookies(...*Cookie)` | Response cookies. |
| `Body(string)`, `Bodyf(format, args...)`, `BodyFromFile(path)`, `JSON(any)` | The body. |
| `Times(int)`, `AnyTimes()` | How many calls the mock answers. [Call counts](/docs/mocks/call-counts) |
| `Timeout()`, `FixedDelay(ms int64)` | Fail with a timeout, or respond slowly. [Failures and latency](/docs/mocks/failures-and-latency) |
| `End()` | Finish the mock. |
| `EndStandalone(...*Mock) func()` | Install as a [standalone mock](/docs/mocks/standalone) and return the reset function. |
## StandaloneMocks [#standalonemocks]
`NewStandaloneMocks` returns a `*StandaloneMocks`. See [standalone mocks](/docs/mocks/standalone).
| Function or method | Description |
| --- | --- |
| `NewStandaloneMocks(...*Mock)` | Group mocks for standalone use. |
| `HttpClient(*http.Client)`, `Debug()` | Options. |
| `End() func()` | Install the mocks and return the reset function. |
## Cookie [#cookie]
| Function or method | Description |
| --- | --- |
| `NewCookie(name)` | Start a cookie. |
| `Value`, `Path`, `Domain`, `Expires`, `MaxAge`, `Secure`, `HttpOnly` | Set a field. Unset fields are ignored in comparisons. |
| `FromHTTPCookie(*http.Cookie)`, `ToHttpCookie()` | Convert to and from `*http.Cookie`. |
## Reports [#reports]
| Name | Description |
| --- | --- |
| `SequenceDiagram(path ...string)` | Returns a `*SequenceDiagramFormatter`, the HTML [sequence diagram](/docs/reports/sequence-diagrams) formatter. Its `Format` method writes the report. |
| `ReportFormatter` | The interface for [custom formatters](/docs/reports/custom-reports). |
| `NewTestRecorder()`, `Recorder` | The recorded title, subtitle, meta data and events. |
| `Recorder.AddHttpRequest`, `AddHttpResponse`, `AddMessageRequest`, `AddMessageResponse` | Add events. |
| `Recorder.AddTitle`, `AddSubTitle`, `AddMeta`, `Reset`, `ResponseStatus` | Manage the recorder. |
| `HttpRequest`, `HttpResponse`, `MessageRequest`, `MessageResponse` | The event types. Each implements `Event`, whose `GetTime` returns when it happened. |
| `ConsumerDefaultName`, `SystemUnderTestDefaultName` | The participant names `cli` and `sut`. |
## Types for extension [#types-for-extension]
| Type | Signature |
| --- | --- |
| `Assert` | `func(*http.Response, *http.Request) error` |
| `Matcher` | `func(*http.Request, *MockRequest) error` |
| `Observe` | `func(*http.Response, *http.Request, *APITest)` |
| `Intercept` | `func(*http.Request)` |
| `TestingT` | `Errorf`, `Fatal` and `Fatalf`. [Test frameworks](/docs/guides/test-frameworks) |
| `Verifier` | `Equal`, `True`, `JSONEq`, `Fail` and `NoError`. Implemented by `DefaultVerifier` and `NoopVerifier`. |
| `GraphQLRequestBody` | `Query`, `Variables` and `OperationName`. |
## Ready-made assertions [#ready-made-assertions]
`IsSuccess`, `IsClientError` and `IsServerError` assert that the status code is in the 200 to 399, 400 to 499, or 500 and above range.
## Internal and deprecated [#internal-and-deprecated]
These are exported but rarely needed in tests.
| Name | Description |
| --- | --- |
| `Transport` | The `http.RoundTripper` that answers mocked calls. `Hijack` installs it, `Reset` restores the original transport, and `RoundTrip` matches a request against the mocks. apitest manages it for you. |
| `OSFS` | The default `fs.FS` behind `UseFS`. `Open` reads from the operating system. |
| `InboundRequest`, `FinalResponse` | Deprecated. Not used by apitest, and kept for compatibility. |
| `RecorderHook` | Deprecated. Use `Recorder`. |
## Sub-packages [#sub-packages]
| Package | Contents |
| --- | --- |
| `github.com/steinfletcher/apitest/x/db` | `WrapWithRecorder` and `WrapConnectorWithRecorder`. [Database interactions](/docs/reports/database) |
| `github.com/steinfletcher/apitest/mocks` | `MockVerifier`, a stubbable `Verifier`. |
# Companion libraries (/docs/reference/companion-libraries)
apitest has no dependencies. Features that need one are published as separate modules, so you only add what you use.
## JSONPath [#jsonpath]
[github.com/steinfletcher/apitest-jsonpath](https://github.com/steinfletcher/apitest-jsonpath) asserts on parts of JSON responses, and matches JSON bodies sent to mocks.
```bash
go get github.com/steinfletcher/apitest-jsonpath
```
See the [JSONPath guide](/docs/assertions/jsonpath).
## CSS selectors [#css-selectors]
[github.com/steinfletcher/apitest-css-selector](https://github.com/steinfletcher/apitest-css-selector) asserts on HTML responses with CSS selectors, for server rendered pages.
```bash
go get github.com/steinfletcher/apitest-css-selector
```
```go
apitest.Handler(handler).
Get("/products/gopher-plush").
Expect(t).
Status(http.StatusOK).
Assert(selector.TextExists("Gopher plush")).
Assert(selector.FirstTextValue(`h1.title`, "Gopher plush")).
Assert(selector.ContainsTextValue(selector.DataTestID("price"), "£12.99")).
Assert(selector.NthTextValue(1, `.reviews li`, "Soft")).
Assert(selector.Exists(`.reviews`)).
Assert(selector.NotExists(`.out-of-stock`)).
End()
```
| Assertion | Passes when |
| --- | --- |
| `selector.TextExists(text)` | The text appears anywhere in the document. |
| `selector.FirstTextValue(sel, text)` | The first element matching the selector has the text. |
| `selector.NthTextValue(n, sel, text)` | The nth element matching the selector, counting from zero, has the text. |
| `selector.ContainsTextValue(sel, text)` | An element matching the selector contains the text. |
| `selector.Exists(sel...)`, `selector.NotExists(sel...)` | Elements matching the selectors exist, or do not. |
| `selector.Selection(sel, func(*goquery.Selection) error)` | Your function, given the matching elements, returns nil. |
| `selector.DataTestID(id)` | Builds the selector `[data-test-id="id"]`. |
## PlantUML [#plantuml]
[github.com/steinfletcher/apitest-plantuml](https://github.com/steinfletcher/apitest-plantuml) formats reports as [PlantUML](https://plantuml.com) sequence diagrams, which can be rendered in documentation tools and wikis.
```bash
go get github.com/steinfletcher/apitest-plantuml
```
```go
apitest.New("search user").
Report(plantuml.NewFormatter(writer)).
Mocks(getUser, getPreferences).
Handler(handler).
Post("/user/search").
JSON(`{"name": "jon"}`).
Expect(t).
Status(http.StatusOK).
End()
```
## DynamoDB [#dynamodb]
[github.com/steinfletcher/apitest-dynamodb](https://github.com/steinfletcher/apitest-dynamodb) records DynamoDB calls made with the AWS SDK, so they appear in [sequence diagrams](/docs/reports/sequence-diagrams) alongside HTTP calls.
## Writing your own [#writing-your-own]
Assertions are `func(*http.Response, *http.Request) error`, mock matchers are `func(*http.Request, *apitest.MockRequest) error`, and report formatters implement `ReportFormatter`. See [custom assertions](/docs/assertions/custom), [custom matchers](/docs/mocks/custom-matchers) and [custom reports](/docs/reports/custom-reports).
# Examples (/docs/reference/examples)
## Example projects [#example-projects]
The [examples directory](https://github.com/steinfletcher/apitest/tree/master/examples) of the apitest repository has complete, runnable projects.
| Example | Shows |
| --- | --- |
| [gin](https://github.com/steinfletcher/apitest/tree/master/examples/gin) | A gin application. |
| [echo](https://github.com/steinfletcher/apitest/tree/master/examples/echo) | An echo application. |
| [fiber](https://github.com/steinfletcher/apitest/tree/master/examples/fiber) | A fiber application, adapted to `net/http`. |
| [gorilla](https://github.com/steinfletcher/apitest/tree/master/examples/gorilla) | The gorilla web toolkit. |
| [httprouter](https://github.com/steinfletcher/apitest/tree/master/examples/httprouter) | julienschmidt/httprouter. |
| [iris](https://github.com/steinfletcher/apitest/tree/master/examples/iris) | An iris application. |
| [graphql](https://github.com/steinfletcher/apitest/tree/master/examples/graphql) | A gqlgen GraphQL server. |
| [ginkgo](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo) | Tests written as Ginkgo specs. |
| [mocks](https://github.com/steinfletcher/apitest/tree/master/examples/mocks) | Mocking outbound 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) | Generating sequence diagrams. |
| [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 in sequence diagrams. |
## Live reports [#live-reports]
Reports generated by the examples on this site:
## The examples behind these docs [#the-examples-behind-these-docs]
Every code sample on this site is included from a Go module of tests that run against the latest apitest release. The samples are therefore real, compiling code, and each one passes.
# Troubleshooting (/docs/reference/troubleshooting)
The chain must end with `End()`. Without it the test is only described, never run, and nothing can fail.
No mock matched the outbound request. Add `Debug()` to the test to see each mock and the reason it was rejected. The usual causes are a different path or host, a header or query value that does not match, or a mock that already answered its call. See [debugging a mismatch](/docs/mocks/matching#debugging-a-mismatch).
The code under test uses an `http.Client` with its own `Transport`, so it does not go through `http.DefaultTransport`. Pass that client to the test with `HttpClient`. See [HTTP clients](/docs/mocks/http-clients).
Mock paths, and header, query and form values, are unanchored regular expressions. `Query("page", "1")` matches `page=10`, and `Get("http://users/api/user")` matches `/api/user/1234`. Anchor them with `^` and `$`. See [paths are regular expressions](/docs/mocks/matching#paths-are-regular-expressions).
The mock has no `Status`. A mock response defaults to status code `0`, so always set one.
Versions before v1.7.0 discard a query string written into the URL, as in `Get("/articles?page=2")`. Upgrade, or add the parameters with `Query`, `QueryParams` or `QueryCollection`. See [query parameters](/docs/requests/query-parameters).
Mocks replace `http.DefaultTransport`, which is shared by the whole process, so parallel tests overwrite each other's mocks. Remove `t.Parallel()`, or give each test its own client and pass it with `HttpClient`. See [parallel tests](/docs/mocks/http-clients#parallel-tests).
JSON numbers decode as `float64`. Compare with `float64(12345)`, not the integer `12345`.
`Present` requires a non-empty value, so a field that is `false`, `0`, `""`, `null` or an empty array counts as not present. Use `Equal` to check those values.
Delays are off by default. Add `EnableMockResponseDelay()` to the test. A delay also has no effect on a mock with `Timeout()`, and it does not trigger client timeouts. Use `Timeout()` for that. See [failures and latency](/docs/mocks/failures-and-latency).
Bodies that are not valid JSON are compared exactly, so a trailing newline matters. JSON bodies are compared structurally and are not affected.
The recording driver records everything, including seed data. Seed through the unwrapped driver, or call `recorder.Reset()` before the test. See [database interactions](/docs/reports/database#keep-setup-out-of-the-diagram).
## Getting help [#getting-help]
Ask questions in #apitest on the [Gophers Slack](https://gophers.slack.com), and report bugs or request features on [GitHub](https://github.com/steinfletcher/apitest/issues).
# Custom reports (/docs/reports/custom-reports)
## Report formatters [#report-formatters]
`Report` accepts anything that implements `apitest.ReportFormatter`:
```go title="apitest.ReportFormatter"
type ReportFormatter interface {
Format(*apitest.Recorder)
}
```
`Format` is called once the test has run, with a `Recorder` that holds everything that happened. This formatter writes a short Markdown summary:
```go
// markdownFormatter writes a short Markdown summary of each test.
type markdownFormatter struct {
out *strings.Builder
}
func (f markdownFormatter) Format(r *apitest.Recorder) {
fmt.Fprintf(f.out, "## %s\n\n%s\n\n", r.Title, r.SubTitle)
for i, event := range r.Events {
switch e := event.(type) {
case apitest.HttpRequest:
fmt.Fprintf(f.out, "%d. %s → %s: `%s %s`\n", i+1, e.Source, e.Target, e.Value.Method, e.Value.URL)
case apitest.HttpResponse:
fmt.Fprintf(f.out, "%d. %s → %s: `%d`\n", i+1, e.Source, e.Target, e.Value.StatusCode)
case apitest.MessageRequest:
fmt.Fprintf(f.out, "%d. %s → %s: %s\n", i+1, e.Source, e.Target, e.Header)
case apitest.MessageResponse:
fmt.Fprintf(f.out, "%d. %s → %s: %s\n", i+1, e.Source, e.Target, e.Header)
}
}
fmt.Fprintf(f.out, "\nstatus %v in %v\n", r.Meta["status_code"], time.Duration(r.Meta["duration"].(int64)))
}
func TestReport_CustomFormatter(t *testing.T) {
var out strings.Builder
apitest.New("gets a user").
Report(markdownFormatter{out: &out}).
Mocks(getUserMock, getPreferencesMock).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
t.Log(out.String())
}
```
```markdown title="Output"
## GET /user/1234
gets a user
1. cli → sut: `GET /user/1234`
2. sut → users: `GET http://users/api/user/1234`
3. users → sut: `200`
4. sut → preferences: `GET http://preferences/api/preferences/1234`
5. preferences → sut: `200`
6. sut → cli: `200`
status 200 in 504.791µs
```
## The recorder [#the-recorder]
```go title="apitest.Recorder"
type Recorder struct {
Title string // the method and URL of the request
SubTitle string // the name passed to apitest.New
Meta map[string]any // see below
Events []Event // in the order they happened
}
```
Each event is one of four types, which all have `Source`, `Target` and `Timestamp` fields:
| Event | Records | Payload |
| --- | --- | --- |
| `apitest.HttpRequest` | The request into the handler, and each request to a mock. | `Value *http.Request` |
| `apitest.HttpResponse` | Each response from a mock, and the final response. | `Value *http.Response` |
| `apitest.MessageRequest` | A custom event, such as a SQL query. | `Header`, `Body` strings |
| `apitest.MessageResponse` | A custom response, such as a SQL result. | `Header`, `Body` strings |
`Meta` holds the values passed to [`Meta`](/docs/reports/sequence-diagrams#naming-participants-and-choosing-a-directory), plus these keys that apitest adds:
| Key | Value |
| --- | --- |
| `name` | The test name. |
| `method`, `path` | The method and URL of the request. |
| `status_code` | The final status code. |
| `duration` | How long the test took, in nanoseconds, as an `int64`. |
| `hash` | A stable identifier for the test, used to name report files. |
## Custom events [#custom-events]
Code running inside the test can add its own events, which appear in every report including sequence diagrams. Create a recorder, give it to the code that should record, and pass the same recorder to the test with `Recorder`:
```go
// Queue publishes messages and records each one on the test recorder.
type Queue struct {
recorder *apitest.Recorder
}
func (q *Queue) Publish(topic, message string) {
// ... publish to the real broker ...
if q.recorder != nil {
q.recorder.AddMessageRequest(apitest.MessageRequest{
Source: apitest.SystemUnderTestDefaultName,
Target: "kafka",
Header: "publish " + topic,
Body: message,
Timestamp: time.Now().UTC(),
})
}
}
func TestCreateOrder_PublishesEvent(t *testing.T) {
recorder := apitest.NewTestRecorder()
queue := &Queue{recorder: recorder}
apitest.New("creates an order").
Recorder(recorder).
Report(apitest.SequenceDiagram()).
Handler(newOrderAPI(queue)).
Post("/orders").
JSON(`{"sku": "gopher-plush", "quantity": 1}`).
Expect(t).
Status(http.StatusAccepted).
End()
}
```
The [`x/db` package](/docs/reports/database) records SQL queries in exactly this way. Other uses include message queues, caches, and SDK calls that do not go over HTTP.
## Companion formatters [#companion-formatters]
[apitest-plantuml](/docs/reference/companion-libraries#plantuml) formats reports as PlantUML markup.
# Database interactions (/docs/reports/database)
The `x/db` package wraps any `database/sql` driver so that the queries made during a test are recorded and drawn in the [sequence diagram](/docs/reports/sequence-diagrams).
## Setup [#setup]
```go
import apitestdb "github.com/steinfletcher/apitest/x/db"
```
### Wrap and register the driver [#wrap-and-register-the-driver]
Create a recorder, wrap the driver you already use, and register the wrapped driver under a new name. This example uses the pure Go `modernc.org/sqlite` driver, which registers itself as `sqlite`.
```go
var recorder = apitest.NewTestRecorder()
func init() {
// wrap the driver, then register the wrapped version under a new name
sql.Register("sqlite-recorded", apitestdb.WrapWithRecorder("sqlite", recorder))
}
```
### Open the database through the wrapped driver [#open-the-database-through-the-wrapped-driver]
Open the application's database with the new driver name, and pass the same recorder to the test with `Recorder`:
```go
func TestGetTodo(t *testing.T) {
dsn := filepath.Join(t.TempDir(), "todos.db")
seedTodos(t, dsn)
// the app opens the database through the recording driver
db, err := sql.Open("sqlite-recorded", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
apitest.New("gets a todo").
Recorder(recorder).
Report(apitest.SequenceDiagram()).
Handler(newTodoAPI(db)).
Get("/todos/1").
Expect(t).
Status(http.StatusOK).
Body(`{"id": 1, "title": "write docs", "done": false}`).
End()
}
// seedTodos uses the plain driver so the setup queries stay out of the diagram.
func seedTodos(t *testing.T, dsn string) {
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT, done BOOLEAN);
INSERT INTO todos (title, done) VALUES ('write docs', false);
`)
if err != nil {
t.Fatal(err)
}
}
```
Each query appears as a `SQL Query` message with its arguments, followed by a `SQL Result` message with the number of rows returned or affected. The participant is named after the wrapped driver.
## Keep setup out of the diagram [#keep-setup-out-of-the-diagram]
Everything the wrapped driver does is recorded, including queries made before the test runs. The example seeds the database through the plain `sqlite` driver so that the inserts do not appear in the diagram. Alternatively, call `recorder.Reset()` after seeding. The recorder is reset after each report, so tests that share it start clean.
## Connectors [#connectors]
Drivers that are opened with a `driver.Connector`, such as `pgx` through `stdlib.GetConnector`, can be wrapped with `WrapConnectorWithRecorder`. The second argument names the participant:
```go
connector := apitestdb.WrapConnectorWithRecorder(pgxConnector, "postgres", recorder)
db := sql.OpenDB(connector)
```
## Examples [#examples]
The apitest repository has complete examples for [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 [PostgreSQL](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams-with-postgres-database).
For DynamoDB, see the [apitest-dynamodb](/docs/reference/companion-libraries#dynamodb) companion library.
# Sequence diagrams (/docs/reports/sequence-diagrams)
`Report(apitest.SequenceDiagram())` turns a test into an HTML page: a sequence diagram of every interaction, next to the wire representation of each message.
```go
apitest.New("gets a user").
Report(apitest.SequenceDiagram()).
Mocks(getUserMock, getPreferencesMock).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
When the test finishes, apitest prints where it wrote the report:
```text
Created sequence diagram (3490420317_3914572186.html): /home/jon/user/.sequence/3490420317_3914572186.html
```
## Reading a report [#reading-a-report]
- **The diagram** has a column per participant and an arrow per message, numbered in order. Requests are solid and responses are dashed, coloured by status code.
- **The events** list each message with its start line, headers and body. JSON bodies are indented and highlighted, and long bodies can be expanded.
- **Click an arrow** to jump to its message, or a message to highlight its arrow. j and k move between messages.
- **Click a participant** to show only the messages it sent or received, or type in the filter to search every message.
- **Copy** any message as raw text, or a request as a `curl` command, to replay it by hand.
- **Calls that matched no mock** have no response, so the report marks them. See [debugging a mismatch](/docs/mocks/matching#debugging-a-mismatch).
The report follows your system's light or dark setting, and has a toggle. It works on narrow screens, and printing it expands every message.
Each report is a single HTML file with its styles, scripts and diagram inline. It loads nothing from the network, so it can be opened offline or kept as a CI artifact. The diagram is drawn as SVG when the report is written, so it shows even where JavaScript is disabled.
This report arrived in apitest v1.8.0. Earlier versions write a simpler page with the same diagram, which loads its scripts from public CDNs.
## What is recorded [#what-is-recorded]
| Participant | Default name |
| --- | --- |
| The test, which sends the request | `cli` |
| The handler under test | `sut` |
| Each mocked dependency | The host of the request, such as `users` |
| Each database, with [`x/db`](/docs/reports/database) | The driver name, such as `sqlite` |
The page title is the method and URL of the request, and the subtitle is the name passed to `apitest.New`. Give every reported test a name, so the reports are easy to tell apart.
## Naming participants and choosing a directory [#naming-participants-and-choosing-a-directory]
Pass a directory to `SequenceDiagram` to change where reports are written, and set the `consumerName` and `systemUnderTestName` meta values to rename `cli` and `sut`:
```go
apitest.New("gets a user").
Report(apitest.SequenceDiagram(".sequence-diagrams")).
Meta(map[string]any{
"consumerName": "web-app",
"systemUnderTestName": "user-api",
}).
Mocks(getUserMock, getPreferencesMock).
Handler(newUserAPI()).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
## Reporting every test [#reporting-every-test]
Configure reporting once in a helper, so every test in the package produces a report:
```go
// newAPITest configures the reporting once for every test in the package.
func newAPITest(name string) *apitest.APITest {
return apitest.New(name).
Report(apitest.SequenceDiagram()).
Meta(map[string]any{"systemUnderTestName": "user-api"}).
Handler(newUserAPI())
}
func TestGetUser_Reported(t *testing.T) {
newAPITest("gets a user").
Mocks(getUserMock, getPreferencesMock).
Get("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
}
```
## Report files [#report-files]
- Files are named after a hash of the method, path and test name, so a test overwrites its own report on each run.
- The default directory is `.sequence`, relative to the package. Add it to `.gitignore`.
- Reports are self-contained, so a directory of them can be zipped, uploaded or served as it is.
Reports make useful build artifacts. Upload the `.sequence` directories from CI to give reviewers a picture of what each test does.
## Beyond HTTP [#beyond-http]
- Add database queries to the diagram with the [`x/db` package](/docs/reports/database).
- Add your own events, such as messages published to a queue, with [custom events](/docs/reports/custom-reports#custom-events).
- Produce a different format, such as PlantUML or Markdown, with a [custom formatter](/docs/reports/custom-reports).
# Authentication (/docs/requests/authentication)
## Basic auth [#basic-auth]
`BasicAuth` sets the `Authorization` header with the encoded username and password:
```go
apitest.Handler(handler).
Get("/admin").
BasicAuth("username", "password").
Expect(t).
Status(http.StatusOK).
End()
```
## Bearer tokens and API keys [#bearer-tokens-and-api-keys]
Tokens are ordinary headers:
```go
apitest.Handler(handler).
Get("/me").
Header("Authorization", "Bearer "+token).
Expect(t).
Status(http.StatusOK).
End()
```
If every test needs the same credentials, put them in a [helper that configures the test](/docs/guides/table-driven-tests#shared-configuration). When the handler expects a signature computed from the final request, compute it in an [interceptor](/docs/requests/intercept).
## Asserting on tokens in the response [#asserting-on-tokens-in-the-response]
The [JSONPath module](/docs/assertions/jsonpath#jwt) can decode a JWT returned by a login endpoint and assert on its header and claims.
# Body and JSON (/docs/requests/body)
## Raw body [#raw-body]
`Body` sends a string as the request body. Set the content type separately:
```go
apitest.Handler(handler).
Post("/message").
Body("
hello
").
ContentType("text/html").
Expect(t).
Status(http.StatusOK).
End()
```
`Bodyf` formats the body with `fmt.Sprintf`:
```go
apitest.Handler(formatted).
Post("/user").
Bodyf(`{"id": "%s"}`, id).
Expect(t).
Status(http.StatusOK).
End()
```
## JSON [#json]
`JSON` sets the body and the `Content-Type: application/json` header. It accepts a string or a `[]byte`, which are sent unchanged:
```go
apitest.Handler(handler).
Post("/user").
JSON(`{"name": "jan"}`).
Expect(t).
Status(http.StatusOK).
End()
```
Any other value is marshalled with `encoding/json`, so structs, maps and slices all work:
```go
type CreateUser struct {
Name string `json:"name"`
Age int `json:"age"`
}
apitest.Handler(handler).
Post("/user").
JSON(CreateUser{Name: "jan", Age: 32}).
Expect(t).
Status(http.StatusOK).
End()
apitest.Handler(handler).
Post("/user").
JSON(map[string]any{"name": "jan", "age": 32}).
Expect(t).
Status(http.StatusOK).
End()
```
If marshalling fails, the test fails when `Expect` is called.
## Bodies from files [#bodies-from-files]
Large or shared payloads are easier to keep in files. `JSONFromFile` reads the body and sets the JSON content type, and `BodyFromFile` reads the body only:
```go
apitest.Handler(handler).
Post("/user").
JSONFromFile("testdata/create-user.json").
Expect(t).
Status(http.StatusOK).
End()
```
```go
apitest.Handler(raw).
Post("/import").
BodyFromFile("testdata/users.csv").
ContentType("text/csv").
Expect(t).
Status(http.StatusOK).
End()
```
Paths are relative to the package directory, which is the working directory when `go test` runs. A `testdata` directory is the usual place for these files, because the Go tool ignores it when building.
## Other content types [#other-content-types]
For XML, HTML, CSV, protobuf or anything else, combine `Body` with `ContentType` or `Header("Content-Type", ...)`. For form and multipart bodies, use the dedicated [form builders](/docs/requests/forms).
# Context (/docs/requests/context)
`WithContext` sets the request context. The handler receives it from `r.Context()`, so deadlines, cancellation and context values all reach it:
```go
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ctx = context.WithValue(ctx, ctxKey("tenant"), "acme")
apitest.Handler(handler).
Get("/hello").
WithContext(ctx).
Expect(t).
Status(http.StatusOK).
End()
```
Common uses are:
- Checking that a handler stops work when the client goes away, by cancelling the context.
- Checking that a handler respects a deadline.
- Injecting values that middleware would normally add, such as an authenticated user, when testing a handler without its middleware.
To check what a client sees when a handler is too slow, see [handler timeouts](/docs/guides/timeouts).
# Cookies (/docs/requests/cookies)
```go
apitest.Handler(handler).
Get("/hello").
Cookie("session", "12345").
Cookies(
apitest.NewCookie("theme").Value("dark"),
).
Expect(t).
Status(http.StatusOK).
End()
```
| Method | Behaviour |
| --- | --- |
| `Cookie(name, value)` | Adds a cookie. |
| `Cookies(...*apitest.Cookie)` | Adds each cookie. The function is variadic. |
## Building cookies [#building-cookies]
`apitest.NewCookie(name)` returns a builder with the methods `Value`, `Path`, `Domain`, `Expires`, `MaxAge`, `Secure` and `HttpOnly`. A request only carries the name and value of a cookie to the server, so the other attributes matter mainly when [asserting on response cookies](/docs/assertions/cookies).
Every field of `apitest.Cookie` is a pointer, so a field that was never set is distinguishable from its zero value. Assertions use this to compare only the fields you set.
## Converting an http.Cookie [#converting-an-httpcookie]
`apitest.FromHTTPCookie` converts a `*http.Cookie`, for example one returned by a login step:
```go
sessionCookie := &http.Cookie{Name: "session", Value: "12345"}
apitest.Handler(handler).
Get("/hello").
Cookies(apitest.FromHTTPCookie(sessionCookie)).
Expect(t).
Status(http.StatusOK).
End()
```
`ToHttpCookie` converts the other way.
## Sessions across requests [#sessions-across-requests]
Each apitest test sends one request. To carry cookies from one request to the next, such as a session cookie set at login, run against a real server with a cookie jar. See [end-to-end tests](/docs/guides/end-to-end).
# Forms and file uploads (/docs/requests/forms)
## URL encoded forms [#url-encoded-forms]
`FormData` sends an `application/x-www-form-urlencoded` body and sets the content type. It takes one or more values for a key, and can be called repeatedly:
```go
apitest.Handler(handler).
Post("/signup").
FormData("name", "jan").
FormData("colors", "red", "blue").
Expect(t).
Status(http.StatusOK).
End()
```
The body in this example is `colors=red&colors=blue&name=jan`.
## Multipart forms [#multipart-forms]
`MultipartFormData` adds fields to a `multipart/form-data` body, and `MultipartFile` adds files. The content type, including the boundary, is set for you.
```go
apitest.Handler(handler).
Post("/upload").
MultipartFormData("description", "monthly reports").
MultipartFile("file", "testdata/users.csv", "testdata/summary.txt").
Expect(t).
Status(http.StatusOK).
End()
```
`MultipartFile` takes a field name and one or more file paths. Each file is sent with its base name as the file name, so the handler sees `users.csv` and `summary.txt`.
`FormData` and the multipart builders cannot be combined in one request. Doing so fails the test.
## Files from memory [#files-from-memory]
Files are read from the operating system by default. `UseFS` replaces the file system with any `fs.FS`, such as an in-memory `fstest.MapFS` or an `embed.FS`:
```go
files := fstest.MapFS{
"audio.wav": &fstest.MapFile{Data: []byte{19, 2, 123, 12, 35, 1}},
}
apitest.Handler(handler).
UseFS(files).
Post("/upload").
MultipartFile("file", "audio.wav").
Expect(t).
Status(http.StatusOK).
End()
```
This keeps binary fixtures out of the repository and makes it easy to generate uploads in the test.
# GraphQL (/docs/requests/graphql)
GraphQL requests are JSON bodies with a `query`, optional `variables` and an optional `operationName`. apitest builds the body and sets `Content-Type: application/json`.
## GraphQLQuery [#graphqlquery]
`GraphQLQuery` takes the query and an optional map of variables:
```go
apitest.Handler(handler).
Post("/graphql").
GraphQLQuery(
`query User($id: ID!) { user(id: $id) { name } }`,
map[string]any{"id": "1234"},
).
Expect(t).
Status(http.StatusOK).
Body(`{"data": {"user": {"name": "jon"}}}`).
End()
```
## GraphQLRequest [#graphqlrequest]
`GraphQLRequest` takes the full `apitest.GraphQLRequestBody`, which is needed to set the operation name:
```go
apitest.Handler(handler).
Post("/graphql").
GraphQLRequest(apitest.GraphQLRequestBody{
Query: `query User($id: ID!) { user(id: $id) { name } }`,
Variables: map[string]any{"id": "1234"},
OperationName: "User",
}).
Expect(t).
Status(http.StatusOK).
Body(`{"data": {"user": {"name": "jon"}}}`).
End()
```
```go title="GraphQLRequestBody"
type GraphQLRequestBody struct {
Query string `json:"query"`
Variables map[string]any `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
}
```
## Asserting on GraphQL responses [#asserting-on-graphql-responses]
GraphQL servers usually respond `200 OK` even when the query fails, and report problems in an `errors` array. Assert on the body, or on parts of it with [JSONPath](/docs/assertions/jsonpath), for example `jsonpath.NotPresent("$.errors")`.
The apitest repository has a complete [gqlgen example](https://github.com/steinfletcher/apitest/tree/master/examples/graphql).
# Headers (/docs/requests/headers)
```go
apitest.Handler(handler).
Get("/hello").
Header("Authorization", "Bearer token").
Headers(map[string]string{
"X-Request-Id": "12345",
"X-Tenant": "acme",
}).
ContentType("application/json").
Expect(t).
Status(http.StatusOK).
End()
```
| Method | Behaviour |
| --- | --- |
| `Header(key, value)` | Adds a value for the header. |
| `Headers(map[string]string)` | Adds a value for each entry in the map. |
| `ContentType(value)` | Sets `Content-Type`, replacing any earlier value. |
The methods can be combined and called as often as needed.
## Repeated headers [#repeated-headers]
`Header` and `Headers` add values rather than replace them, so calling `Header` twice with the same key sends both values:
```go
apitest.Handler(handler).
Get("/hello").
Header("Accept", "application/json").
Header("Accept", "text/plain").
Expect(t).
Status(http.StatusOK).
End()
```
## Header names [#header-names]
Header names are stored in their canonical form, so `x-request-id` and `X-Request-Id` refer to the same header. This matches how Go's `net/http` treats headers.
## Headers set for you [#headers-set-for-you]
Some request builders set `Content-Type` themselves:
| Builder | Content-Type |
| --- | --- |
| [`JSON`, `JSONFromFile`](/docs/requests/body) | `application/json` |
| [`GraphQLQuery`, `GraphQLRequest`](/docs/requests/graphql) | `application/json` |
| [`FormData`](/docs/requests/forms) | `application/x-www-form-urlencoded` |
| [`MultipartFormData`, `MultipartFile`](/docs/requests/forms) | `multipart/form-data` with the boundary |
[`BasicAuth`](/docs/requests/authentication) sets the `Authorization` header and [`Cookie`](/docs/requests/cookies) sets the `Cookie` header.
# Building requests (/docs/requests)
## The handler under test [#the-handler-under-test]
`Handler` accepts any `http.Handler` and `HandlerFunc` accepts a plain handler function. Routers from chi, gin, echo, gorilla and most other frameworks implement `http.Handler`, so they can be passed in directly. See [frameworks](/docs/guides/frameworks).
```go
// any http.Handler: a *http.ServeMux, a gin engine, a chi router...
apitest.New().
Handler(handler).
Get("/health").
Expect(t).
Status(http.StatusOK).
End()
// or a plain handler function
apitest.New().
HandlerFunc(myHandlerFunc).
Get("/health").
Expect(t).
Status(http.StatusOK).
End()
```
`apitest.Handler` and `apitest.HandlerFunc` are shorthand for `apitest.New().Handler(...)`:
```go
apitest.Handler(handler).
Get("/health").
Expect(t).
Status(http.StatusOK).
End()
```
apitest calls the handler's `ServeHTTP` method in the same process as the test, with a `httptest.ResponseRecorder` as the response writer. No network connection is made, which keeps tests fast and deterministic. To test a running server instead, see [end-to-end tests](/docs/guides/end-to-end).
If the handler panics, apitest recovers, then fails the test with the panic value and a stack trace.
## Method and URL [#method-and-url]
`Get`, `Post`, `Put`, `Patch` and `Delete` set the method and URL in one call.
```go
apitest.Handler(handler).Get("/user/1234").Expect(t).Status(http.StatusOK).End()
apitest.Handler(handler).Post("/user/1234").Expect(t).Status(http.StatusOK).End()
apitest.Handler(handler).Put("/user/1234").Expect(t).Status(http.StatusOK).End()
apitest.Handler(handler).Patch("/user/1234").Expect(t).Status(http.StatusOK).End()
apitest.Handler(handler).Delete("/user/1234").Expect(t).Status(http.StatusOK).End()
```
Each has an `f` variant that formats the URL with `fmt.Sprintf`:
```go
id := "1234"
apitest.Handler(handler).
Getf("/user/%s", id).
Expect(t).
Status(http.StatusOK).
End()
```
`Method` covers every other verb. Set the URL with `URL` or `URLf`:
```go
apitest.Handler(handler).
Method(http.MethodOptions).
URL("/user/1234").
Expect(t).
Status(http.StatusOK).
End()
```
```go
apitest.Handler(handler).
Method(http.MethodOptions).
URLf("/user/%d", 1234).
Expect(t).
Status(http.StatusOK).
End()
```
The URL is usually just a path, because the request goes straight to the handler. It can include a query string, and [`Query`](/docs/requests/query-parameters) adds more parameters.
## Using an http.Request [#using-an-httprequest]
To build the request yourself, pass a `*http.Request` to `HttpRequest`. The request is used as is, so builder methods such as `Header` and `Query` have no effect on it.
```go
req := httptest.NewRequest(http.MethodGet, "/user/1234", nil)
req.Header.Set("X-Request-Id", "abc")
apitest.Handler(handler).
HttpRequest(req).
Expect(t).
Status(http.StatusOK).
End()
```
## Host [#host]
Requests sent to a handler have the `Host` set to `sut`, short for system under test. Use `Host` when the handler routes or behaves differently by host:
```go
apitest.Handler(handler).
Host("api.example.com").
Get("/hello").
Expect(t).
Status(http.StatusOK).
End()
```
## In this section [#in-this-section]
# Intercept (/docs/requests/intercept)
`Intercept` receives the `*http.Request` that apitest built, just before it is sent to the handler. Anything the builder cannot express can be set here.
```go
apitest.Handler(handler).
Intercept(func(req *http.Request) {
req.URL.RawQuery = "a[]=xxx&a[]=yyy"
req.Header.Set("X-Signature", sign(req))
}).
Get("/hello").
Expect(t).
Status(http.StatusOK).
End()
```
Typical uses:
- Query strings that `url.Values` cannot encode, such as `ids[]=1&ids[]=2`.
- Request signatures computed from the method, path, headers and body.
- Fields of `http.Request` that the builder does not expose, such as `RemoteAddr`.
Changes made by the interceptor are visible to the handler, to [`Debug`](/docs/guides/debugging) output and to [reports](/docs/reports/sequence-diagrams). There is one interceptor per test, and calling `Intercept` again replaces it.
To inspect the request and response after the test has run, rather than change the request, use [`Observe`](/docs/guides/debugging#observe).
# Query parameters (/docs/requests/query-parameters)
```go
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()
```
The request in this example is sent to `/hello?a=1&b=2&c=d&e=f&e=g`.
| Method | Behaviour |
| --- | --- |
| `Query(key, value)` | Adds a value for the key. Call it again with the same key to repeat the parameter. |
| `QueryParams(map[string]string)` | Adds a value for each entry in the map. |
| `QueryCollection(map[string][]string)` | Sets repeated parameters, so `{"e": {"f", "g"}}` becomes `e=f&e=g`. Calling it again replaces the earlier collection. |
All three can be combined in one request.
## Encoding [#encoding]
Parameters added with the builder methods are encoded with Go's `url.Values`, which escapes values and sorts the keys. The order you add them in therefore does not change the URL, which keeps tests stable. A query string written into the URL is sent unchanged.
If the handler expects an encoding `url.Values` cannot produce, such as PHP style `ids[]=1&ids[]=2`, set the raw query with an [interceptor](/docs/requests/intercept):
```go
apitest.Handler(handler).
Intercept(func(req *http.Request) {
req.URL.RawQuery = "ids[]=1&ids[]=2"
}).
Get("/users").
Expect(t).
Status(http.StatusOK).
End()
```
## Query parameters in the URL [#query-parameters-in-the-url]
A query string can also be written into the URL. It is sent as written, and parameters from the builder methods are appended after it, so this request is sent to `/articles?page=2&sort=asc`:
```go
apitest.Handler(handler).
Get("/articles?page=2").
Query("sort", "asc").
Expect(t).
Status(http.StatusOK).
End()
```
Earlier versions discarded a query string written into the URL. On those versions, add every parameter with the builder methods.