Testing and Debugging
Testing and Debugging
Overview
Reliable web services need fast, isolated tests: handlers with httptest, repositories mocked behind interfaces, and a few integration checks. Logging and simple performance checks help you find problems when tests pass but production does not. This chapter closes the Bookstore path with practical routines—not a full testing theory course (see part 06-testing for that).
What to test
| Layer | Technique | Speed |
|---|---|---|
| Domain validation | Table-driven unit tests | Fast |
| Handlers | httptest + mock store |
Fast |
| Repository SQL | Test DB or testcontainers | Medium |
| Full process | httptest server or real port |
Slower |
Aim for many fast tests; keep a thin integration suite.
Table-driven domain tests
func TestBookValidate(t *testing.T) {
tests := []struct {
name string
book domain.Book
wantErr bool
}{
{"ok", domain.Book{Title: "Go", Author: "A", Price: 100}, false},
{"no title", domain.Book{Author: "A", Price: 100}, true},
{"neg price", domain.Book{Title: "Go", Author: "A", Price: -1}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.book.Validate()
if (err != nil) != tt.wantErr {
t.Fatalf("Validate() err=%v wantErr=%v", err, tt.wantErr)
}
})
}
}Mock repository
type mockBooks struct {
list []domain.Book
err error
}
func (m mockBooks) List(ctx context.Context) ([]domain.Book, error) {
return m.list, m.err
}
func (m mockBooks) Get(ctx context.Context, id string) (domain.Book, error) {
return domain.Book{}, errors.New("not implemented")
}
func (m mockBooks) Create(ctx context.Context, b domain.Book) (domain.Book, error) {
return b, m.err
}Or generate mocks later; hand-written stubs stay clear for teaching.
httptest handler test
func TestListBooks(t *testing.T) {
s := &Server{books: mockBooks{list: []domain.Book{
{ID: "1", Title: "Go Book", Author: "Ada", Price: 1999},
}}}
req := httptest.NewRequest(http.MethodGet, "/api/books", nil)
rec := httptest.NewRecorder()
s.listBooks(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code)
}
var got []domain.Book
if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].Title != "Go Book" {
t.Fatalf("unexpected body: %#v", got)
}
}Full mux test
func TestRoutes(t *testing.T) {
s := NewServer(memory.NewBookRepo())
ts := httptest.NewServer(s.Handler())
t.Cleanup(ts.Close)
res, err := http.Get(ts.URL + "/api/books")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("status %d", res.StatusCode)
}
}Testing auth
- Create user + session in the test store.
- Attach cookie:
req.AddCookie(&http.Cookie{Name: "session_id", Value: sid}). - Assert 401 without cookie, 403 with wrong role, 201 with admin.
Keep clocks controllable if you test expiry (inject a now function).
Mocking external APIs
type PaymentClient interface {
Charge(ctx context.Context, in ChargeRequest) (ChargeResponse, error)
}
type fakePay struct {
res ChargeResponse
err error
}
func (f fakePay) Charge(ctx context.Context, in ChargeRequest) (ChargeResponse, error) {
return f.res, f.err
}Handler tests inject fakePay{err: errors.New("down")} and expect 502/503—not a real network call.
Logging and tracing events
Structured logs make incidents greppable:
slog.Info("book_created",
"book_id", book.ID,
"user_id", userID,
"request_id", requestID,
)Request ID middleware
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String() // stdlib uuid (Go 1.27+)
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), reqIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Return the same ID to clients; paste it into log searches during incidents.
Error messages and incident response
| Audience | Message |
|---|---|
| Client | Safe, actionable (validation, not_found) |
| Logs | Full error chain, request id, user id |
| On-call | Runbook: check logs by request id → DB → dependency status |
Do not dump stack traces or SQL to public JSON bodies.
Light performance checks
func BenchmarkListBooks(b *testing.B) {
s := &Server{books: mockBooks{list: make([]domain.Book, 100)}}
b.ResetTimer()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest(http.MethodGet, "/api/books", nil)
rec := httptest.NewRecorder()
s.listBooks(rec, req)
}
}For real hotspots: go test -bench . -cpuprofile=cpu.out and part 09-performance-tooling / 18-performance-engineering.
Smoke load with curl/hey:
hey -n 1000 -c 50 http://localhost:8080/api/booksDebug checklist
1. Reproduce with curl + request id
2. Check status code and error code JSON
3. Search logs for request_id
4. Confirm auth cookie / role
5. Confirm DB row exists
6. Confirm downstream timeout vs 500
7. go test ./... and go test -race ./...
Rules of thumb
| Do | Don’t |
|---|---|
| Mock at interfaces | Hit real payment APIs in unit tests |
| Use httptest for handlers | Require full Docker for every commit |
| Log request ids | Log passwords, session secrets, card numbers |
| Race-test concurrent stores | Assume single-threaded handlers mean no races in packages |
Try next
- Write tests for create book: 201 happy path, 400 validation, 401 unauthenticated.
- Force a panic in a handler and assert recover middleware returns 500.
- Add a failing fake payment client test for checkout.
Where to go next
- Part 08-web — GOTH, gRPC, Huma, Postgres/Redis recipes
- Part 06-testing — organization, integration, advanced patterns
- Part 16-observability-sre — metrics, tracing, correlation
- Part 17-security-hardening — TLS, deeper authn/authz
You now have a clear path from empty module to a testable, session-aware Bookstore API with HTML and JSON edges—simple enough to learn, solid enough to grow.