A Response Test Helper
The Testing in Depth chapter covered table-driven tests and the local-TB
interface trick; Part 2 built the CRUD handlers you've been testing ever
since. By now a pattern has probably shown up in every one of those handler
tests: build a request, record a response with httptest.NewRecorder, call
the handler, then check rec.Code against what you expected. That last step
is almost always the same few lines, copied into every test function.
The repetition
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
One occurrence is fine. Ten handler tests each with their own copy of that
if block is noise — and noise is where bugs hide, because nobody reads
boilerplate carefully. The fix is the same one you'd reach for in production
code: extract the repeated part into a function.
Why t.Helper() matters here
A naive extraction just moves the if block into a function that takes
*testing.T. It works, but it breaks something you'd otherwise take for
granted: when the assertion fails, Go reports the failure's file and line as
inside the helper function, not at the call site in your test. With ten
call sites sharing one helper, every failure points at the same unhelpful
line, and you're left guessing which test actually failed.
t.Helper() fixes that. Called as the first line of a helper function, it
tells the testing package "attribute failures reported from here to my
caller, not to me." The effect is that go test output points straight at
the line in your test that called the helper — exactly where you'd want to
look.
Testing the helper itself
Since the assertion helper needs t.Helper() and t.Errorf, and you want
to unit test that it behaves correctly, you run into the same wall the
Testing in Depth chapter already solved: testing.TB can't be implemented
by your own types, because it has an unexported method. The workaround is
the same — declare a small local interface with just the methods you use:
type TB interface {
Helper()
Errorf(format string, args ...any)
}
*testing.T satisfies this implicitly, so the helper works unmodified in
real tests. A hand-rolled fake that records calls to Helper and Errorf
also satisfies it, which means you can write a test that drives the helper
with the fake and inspect exactly what it recorded — did it call Helper,
did it report a failure, does the failure message mention both the actual
and expected status.
rec.Code
The value under test is httptest.ResponseRecorder.Code, the field the
recorder fills in when the handler calls WriteHeader (or, implicitly, on
the first Write if WriteHeader was never called). Comparing rec.Code
against a want int is the whole assertion — no need for the caller to know
anything about httptest beyond the recorder it already built.
Where this goes next
AssertStatus is one assertion. The same shape — take a TB, take the
recorder, compare one thing, call Helper() first — extends naturally to
AssertJSON (decode the body and compare against an expected value) or
AssertHeader (check a specific response header). Once you have one, adding
siblings is mostly copying the pattern, not inventing a new one.
Practice
1. A Response Test Helper
IntermediateHandler tests repeat
if rec.Code != want { t.Errorf(...) }constantly. A helper cleans that up — andt.Helper()makes failures point at the calling test, not inside the helper.You can't implement
testing.TByourself (unexported method), so this exercise uses a small localTBinterface (Helper,Errorf) that*testing.Tsatisfies — the same trick from the Testing in Depth chapter.Implement
AssertStatus(tb TB, rec *httptest.ResponseRecorder, want int): calltb.Helper()first, then ifrec.Code != want, report viatb.Errorfwith a message containing both the got and want codes.The hidden test drives it with a recording fake TB, checking you called Helper and worded the failure usefully.
package main import "net/http/httptest" // TB is the slice of testing.TB a helper needs. *testing.T satisfies it, so // AssertStatus works in real tests; a recording fake satisfies it too, so we // can test the helper itself. type TB interface { Helper() Errorf(format string, args ...any) } // AssertStatus fails tb (pointing at the caller, via Helper) when the recorded // response status doesn't match want. func AssertStatus(tb TB, rec *httptest.ResponseRecorder, want int) { // TODO: call tb.Helper() first, then if rec.Code != want, report via // tb.Errorf with a message containing both the got and want codes. } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.