Frontend Labs
⋯

Day 29 — Testing: httptest & net.Pipe

Learning Objectives

  • Test HTTP handlers without starting a real server
  • Test TCP protocol code using net.Pipe() — an in-memory bidirectional connection
  • Write integration tests that cover your full middleware chain

Key Concepts

// httptest.NewRecorder — captures response without network
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/api/data?id=1", nil)
handler.ServeHTTP(w, r)
// Assert
assert(w.Code == 200)
assert(w.Header().Get("X-Request-ID") != "")

// httptest.NewServer — real listening server on random port
ts := httptest.NewServer(handler)
defer ts.Close()
resp, err := http.Get(ts.URL + "/api/data")

// httptest.NewTLSServer — TLS server with self-signed cert
ts := httptest.NewTLSServer(handler)
defer ts.Close()
client := ts.Client()  // pre-configured to trust the test cert
resp, err := client.Get(ts.URL + "/api/data")

// net.Pipe — in-memory bidirectional TCP connection (no network)
server, client := net.Pipe()

Exercise: Test the Logger Middleware

// middleware/logger_test.go
package middleware_test

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestLoggerMiddleware(t *testing.T) {
    var logBuffer strings.Builder

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(201)
        w.Write([]byte(`{"created":true}`))
    })

    logged := LoggerMiddleware(&logBuffer)(handler)

    tests := []struct {
        name           string
        method, path   string
        wantStatus     int
        wantLogContains []string
    }{
        {
            name: "GET request logged",
            method: "GET", path: "/api/users",
            wantStatus: 201,
            wantLogContains: []string{"GET", "/api/users", "201", "request_id"},
        },
        {
            name: "request ID injected in response",
            method: "POST", path: "/api/items",
            wantStatus: 201,
            wantLogContains: []string{"POST", "/api/items"},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            logBuffer.Reset()

            w := httptest.NewRecorder()
            r := httptest.NewRequest(tt.method, tt.path, nil)
            logged.ServeHTTP(w, r)

            if w.Code != tt.wantStatus {
                t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
            }

            if w.Header().Get("X-Request-ID") == "" {
                t.Error("X-Request-ID header not set")
            }

            logEntry := logBuffer.String()
            for _, want := range tt.wantLogContains {
                if !strings.Contains(logEntry, want) {
                    t.Errorf("log missing %q in: %s", want, logEntry)
                }
            }

            // Validate log is valid JSON
            var entry map[string]any
            if err := json.Unmarshal([]byte(strings.TrimSpace(logEntry)), &entry); err != nil {
                t.Errorf("log is not valid JSON: %v", err)
            }
        })
    }
}

func TestNetPipe_ProtocolTest(t *testing.T) {
    // Test TCP protocol code without a real network
    server, client := net.Pipe()
    defer server.Close()
    defer client.Close()

    // Server goroutine: echo server
    go func() {
        buf := make([]byte, 1024)
        n, _ := server.Read(buf)
        server.Write(buf[:n])
    }()

    // Client sends and reads echo
    msg := []byte("hello proxy")
    client.Write(msg)
    response := make([]byte, len(msg))
    n, err := client.Read(response)
    if err != nil { t.Fatal(err) }
    if string(response[:n]) != string(msg) {
        t.Errorf("echo = %q, want %q", response[:n], msg)
    }
}

Real-World Context

httptest tests are 100x faster than spinning up a real server. Your CI runs 500 middleware tests per second vs 5 per second with real HTTP. net.Pipe() lets you test your TCP proxy, protocol parser, and TLS handshake without touching the network stack.


Generated from week4.md · 8 Oct 2026 16:36