Skip to content

Go Testing HTTP Handlers — Testing HTTP Servers with httptest and Test Suites

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Go Testing HTTP Handlers. We cover key concepts, practical examples, and best practices to help you master this topic.

Go httptest package provides NewRecorder for handler testing and NewServer for integration testing with real HTTP requests and responses.

What You'll Learn

  • Testing handlers with httptest.NewRecorder
  • Integration tests with httptest.NewServer
  • Testing middleware
  • Testing JSON APIs

Why It Matters

HTTP testing prevents API regressions. Docker tests registry handlers. Kubernetes tests API server handlers. DodaZIP tests REST endpoints.

Real-World Use

API contract testing, integration testing, regression prevention, CI/CD pipeline validation.

flowchart LR
    A["HTTP Testing"] --> B["NewRecorder"]
    A --> C["NewServer"]
    A --> D["Middleware Tests"]
    A --> E["JSON APIs"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Testing Handlers with NewRecorder

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/hello?name=Alice", nil)
    rec := httptest.NewRecorder()

    helloHandler(rec, req)

    if rec.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", rec.Code)
    }

    expected := "Hello, Alice!"
    if rec.Body.String() != expected {
        t.Errorf("expected %q, got %q", expected, rec.Body.String())
    }
}

Testing JSON API

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    user.ID = 1
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func TestCreateUserHandler(t *testing.T) {
    body := `{"name": "Alice"}`
    req := httptest.NewRequest("POST", "/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    rec := httptest.NewRecorder()

    createUserHandler(rec, req)

    if rec.Code != http.StatusCreated {
        t.Errorf("expected 201, got %d", rec.Code)
    }

    var user User
    json.NewDecoder(rec.Body).Decode(&user)
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

Integration Tests with NewServer

func TestServerIntegration(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello, World!")
    }))
    defer server.Close()

    resp, err := http.Get(server.URL)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "Hello, World!" {
        t.Errorf("expected %q, got %q", "Hello, World!", string(body))
    }
}

Testing Middleware

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func TestAuthMiddleware(t *testing.T) {
    handler := authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))

    t.Run("missing token", func(t *testing.T) {
        req := httptest.NewRequest("GET", "/", nil)
        rec := httptest.NewRecorder()
        handler.ServeHTTP(rec, req)
        if rec.Code != http.StatusUnauthorized {
            t.Errorf("expected 401, got %d", rec.Code)
        }
    })

    t.Run("valid token", func(t *testing.T) {
        req := httptest.NewRequest("GET", "/", nil)
        req.Header.Set("Authorization", "Bearer token123")
        rec := httptest.NewRecorder()
        handler.ServeHTTP(rec, req)
        if rec.Code != http.StatusOK {
            t.Errorf("expected 200, got %d", rec.Code)
        }
    })
}

Testing Gin Handlers

func TestGinHandler(t *testing.T) {
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })

    w := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/ping", nil)
    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "pong")
}

Common Mistakes

1. Not Setting Headers

Always set Content-Type on requests. Handlers may depend on header values for Parsing.

2. Ignoring Body Close

defer resp.Body.Close()  // In integration tests

3. Not Testing Error Cases

Test 400, 401, 404, 500 responses. Not just happy paths.

4. State Between Tests

Reset state between test cases. Use t.Run for isolation.

5. Flaky Integration Tests

Don't depend on external services. Mock or use httptest.NewServer for test servers.

Practice Questions

1. What does httptest.NewRecorder do? Records HTTP handler responses. Captures status code, headers, and body for assertions.

2. What does httptest.NewServer do? Starts a real HTTP server for integration testing. Returns server URL. Auto-closes on test completion.

3. How do you test request body parsing? Create request with strings.NewReader(jsonBody), decode response body similarly.

4. How do you test middleware? Wrap a simple handler and assert the middleware behavior for various inputs.

Challenge: Write a test suite for a CRUD API handler with table-driven tests.

Solution
func TestUserAPI(t *testing.T) {
    tests := []struct {
        name   string
        method string
        path   string
        body   string
        code   int
    }{
        {name: "create", method: "POST", path: "/users", body: `{"name":"Alice"}`, code: 201},
        {name: "list", method: "GET", path: "/users", code: 200},
        {name: "invalid", method: "POST", path: "/users", body: `invalid`, code: 400},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
            rec := httptest.NewRecorder()
            handler(rec, req)
            if rec.Code != tt.code {
                t.Errorf("expected %d, got %d", tt.code, rec.Code)
            }
        })
    }
}

FAQ

{{< faq question="What is the difference between NewRecorder and NewServer?" >}} NewRecorder tests handler logic in-Process (fast). NewServer starts a real server (slower but tests the full HTTP stack including routing and middleware). {{< /faq >}}

{{< faq question="How do I test file uploads?" >}} Create multipart form data with bytes.Buffer. Set Content-Type to writer.FormDataContentType(). Send in request body. {{< /faq >}}

{{< faq question="How do I test WebSocket handlers?" >}} Use httptest.NewServer and then gorilla/Websocket or nhooyr.io/websocket to dial the test server. {{< /faq >}}

{{< faq question="Can I test handlers with authentication?" >}} Yes. Set Authorization header on the request. If middleware reads from context, set it before calling ServeHTTP. {{< /faq >}}

{{< faq question="How do I test streaming responses?" >}} Use Flush in your handler. Read from response body progressively. Test with io.Copy and bufio.Scanner. {{< /faq >}}

Try It Yourself

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
}

func TestHello(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    rec := httptest.NewRecorder()
    hello(rec, req)

    if rec.Body.String() != "Hello, World!" {
        t.Errorf("got %q", rec.Body.String())
    }
}

Expected output — test passes.

What's Next

Now that you understand HTTP testing, explore deploying Go applications.

Topic Description Link
Go Deployment Deploying Go apps {{< ref "43-deployment" >}}
Go Middleware HTTP middleware {{< ref "41-middleware" >}}
Go Web Frameworks Web frameworks {{< ref "40-web-frameworks" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go