Testing and Debugging

Updated

September 8, 2026

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

  1. Create user + session in the test store.
  2. Attach cookie: req.AddCookie(&http.Cookie{Name: "session_id", Value: sid}).
  3. 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/books

Debug 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

  1. Write tests for create book: 201 happy path, 400 validation, 401 unauthenticated.
  2. Force a panic in a handler and assert recover middleware returns 500.
  3. 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.