Custom reports
Write your own report formatter, and add your own events to diagrams.
Report formatters
Report accepts anything that implements 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:
// 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())
}
## 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µsThe 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, 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
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:
// 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 records SQL queries in exactly this way. Other uses include message queues, caches, and SDK calls that do not go over HTTP.
Companion formatters
apitest-plantuml formats reports as PlantUML markup.