Anatomy of a test
Every apitest test has three parts, configuration, request and expectations, built in one chain.
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
apitest.New starts a test. The optional name appears in reports. Configuration methods return the test, so they can be chained in any order:
| Method | Purpose |
|---|---|
Handler, HandlerFunc | The handler under test. See requests. |
Mocks | Answers for the outbound HTTP calls the handler makes. See mocks. |
HttpClient | Replace the transport of your own http.Client instead of http.DefaultClient. |
Report, Recorder, Meta | Render the test as a sequence diagram or a custom report. |
Debug | Print every request and response on the wire. See debugging. |
Observe, ObserveMocks | Hooks that receive the final and mocked requests and responses. |
Intercept | Change the built request before it is sent. See intercept. |
EnableNetworking | Send the request over the network to a running server. See end-to-end tests. |
EnableMockResponseDelay | Honour FixedDelay on mocks. See failures and latency. |
Verifier | Replace the assertion implementation. See 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:
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
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.
3. Expectations
Expect(t) ends the request and starts the expectations. It takes the *testing.T, or anything that satisfies apitest.TestingT.
The expectation builder asserts on the status, headers, cookies and body, and accepts custom assertions. End() runs the test and returns a Result.
Remember to call End()
Nothing runs until End() is called. A chain that stops at an expectation compiles, makes no request, and passes.
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.
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
}