CodeRigor

LESSON

Test the whole exchange, not just the status

Build a read-only handler probe that checks status, headers and exact fixture bodies, and prove it rejects plausible broken handlers.

A test that checks only 200 can miss a completely wrong body. A test that checks only a body can miss the wrong status or content type. Define the full exchange contract and compare the fields that matter.

This lesson’s probe invokes the supplied handler directly through httptest.NewRequest and httptest.NewRecorder. No listener, DNS or client socket is involved. That makes the check deterministic and appropriate for the hosted sandbox.

One request yields several assertions
start / 3
The recorder captures the actual status, headers and body. Each assertion establishes a distinct part of the exchange contract.
One request yields several assertions
The recorder captures the actual status, headers and body. Each assertion establishes a distinct part of the exchange contract.

The fixture table is supplied. It includes health GET/HEAD, the empty collection, a method failure and an unknown route. Complete the status/body assertion, then inspect the separate media, nosniff, Allow and HEAD metadata checks.

The probe assumes a fresh empty repository. It is not suitable for an arbitrary populated service, and it does not claim to monitor production uptime. It deliberately performs no create/delete operation.

Go
package main
import ("fmt";"net/http";"net/http/httptest")
func main(){
handler:=http.HandlerFunc(func(w http.ResponseWriter,r *http.Request){w.Header().Set("Content-Type","application/json");w.WriteHeader(404);_,_=w.Write([]byte(`{"error":"not_found"}`))});rec:=httptest.NewRecorder();handler.ServeHTTP(rec,httptest.NewRequest("GET","/missing",nil));fmt.Println(rec.Code,rec.Header().Get("Content-Type"))
}

Prove the probe itself can fail. Feed it plausible broken handlers: one returns success for a missing route, one has the right status and wrong body, one omits Allow, and one writes a HEAD body or wrong representation length. Each should produce an error.

Keep error context useful and bounded: method and known path identify the exchange. Do not paste raw request bodies, credentials or private backend errors into public failure messages.

Handler tests answer a narrow but useful question. The downloadable lab separately tests an ephemeral loopback listener and shutdown adapter. Neither test type proves authentication, persistence or reverse-proxy deployment behavior that the lab does not implement.

A green test needs a demonstrated failure mode
start / 3
The correct handler passes. Known status/body/header mutants fail. Together these show the probe asserts its stated contract.
A green test needs a demonstrated failure mode
The correct handler passes. Known status/body/header mutants fail. Together these show the probe asserts its stated contract.
Go
package main
import ("fmt")
func main(){
expected,actual:="[]\n","null\n";fmt.Println(expected==actual,len(expected),len(actual))
}

Complete the assertion and run both the good fixture and broken candidate table. Check that the probe never writes data and that a body-only defect cannot slip through a status check.

Next we connect routing, bounded decoding, repository calls and JSON responses in the complete service.

Actual handler output · Original fixture

Follow one request through the service.

These seven exchanges were generated by the downloadable Go replay against one fresh in-memory store. Choose a recorded step to inspect its request, response and resulting note count. Choosing a step does not send a live request.

  1. Client requestused
  2. Route & methodused
  3. Bounded decoderused
  4. Repositoryused
  5. JSON responseused
POST /notes201 Created1 stored note after this exchange

Request

Text
POST /notes
Content-Type: application/json
JSON
{
  "title": " learn "
}

Recorded response

JSON
{
  "Content-Length": "25",
  "Content-Type": "application/json; charset=utf-8",
  "Location": "/notes/1",
  "X-Content-Type-Options": "nosniff"
}
JSON
{
  "id": 1,
  "title": "learn"
}

Headers, status, body and stored count are actual recorder results. The highlighted stages annotate the corresponding code path; this is a replay, not a distributed trace. The duplicate-key step leaves the stored count at one.

Download the runnable service, replay and tests →

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Optional design discussion of handlers, dependencies and tests. This 2019 talk predates current Go routing changes; use the current net/http documentation and this course’s explicit route contract for the lab.