1. go
  2. /testing
  3. /http-testing

Testing HTTP Servers and Clients in Go

Go's net/http/httptest package provides powerful tools for testing HTTP servers and clients. This guide covers how to write comprehensive HTTP tests.

Testing HTTP Servers

Basic Server Testing

func TestHandler(t *testing.T) {
    // Create request
    req, err := http.NewRequest("GET", "/api/users", nil)
    if err != nil {
        t.Fatal(err)
    }

    // Create response recorder
    rr := httptest.NewRecorder()
    
    // Create handler
    handler := http.HandlerFunc(UserHandler)
    
    // Serve request
    handler.ServeHTTP(rr, req)
    
    // Check status code
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    
    // Check response body
    expected := `{"status":"success"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

Testing with Query Parameters

func TestHandlerWithParams(t *testing.T) {
    // Create request with query parameters
    req, err := http.NewRequest("GET", "/api/search", nil)
    if err != nil {
        t.Fatal(err)
    }
    
    q := req.URL.Query()
    q.Add("query", "test")
    q.Add("limit", "10")
    req.URL.RawQuery = q.Encode()
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(SearchHandler)
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
}

Testing HTTP Clients

Mock Server

func TestClient(t *testing.T) {
    // Create test server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Verify request
        if r.Method != "POST" {
            t.Errorf("expected POST request, got %s", r.Method)
        }
        
        // Send response
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        fmt.Fprintln(w, `{"id": "123", "status": "created"}`)
    }))
    defer server.Close()
    
    // Use test server URL
    client := NewClient(server.URL)
    resp, err := client.CreateUser(&User{Name: "John"})
    if err != nil {
        t.Fatal(err)
    }
    
    if resp.ID != "123" {
        t.Errorf("expected ID 123, got %s", resp.ID)
    }
}

Testing Request Bodies

func TestClientWithBody(t *testing.T) {
    var receivedBody []byte
    
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var err error
        receivedBody, err = io.ReadAll(r.Body)
        if err != nil {
            t.Fatal(err)
        }
        
        w.WriteHeader(http.StatusOK)
    }))
    defer server.Close()
    
    client := NewClient(server.URL)
    data := map[string]string{"key": "value"}
    
    _, err := client.SendData(data)
    if err != nil {
        t.Fatal(err)
    }
    
    expected := `{"key":"value"}`
    if string(receivedBody) != expected {
        t.Errorf("expected body %s, got %s", expected, string(receivedBody))
    }
}

Advanced Testing

Testing Middleware

func TestAuthMiddleware(t *testing.T) {
    tests := []struct {
        name       string
        token      string
        wantStatus int
    }{
        {
            name:       "valid token",
            token:      "valid-token",
            wantStatus: http.StatusOK,
        },
        {
            name:       "invalid token",
            token:      "invalid-token",
            wantStatus: http.StatusUnauthorized,
        },
        {
            name:       "missing token",
            token:      "",
            wantStatus: http.StatusUnauthorized,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", "/api/protected", nil)
            if tt.token != "" {
                req.Header.Set("Authorization", "Bearer "+tt.token)
            }
            
            rr := httptest.NewRecorder()
            
            // Create handler chain
            handler := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
            }))
            
            handler.ServeHTTP(rr, req)
            
            if rr.Code != tt.wantStatus {
                t.Errorf("handler returned wrong status code: got %v want %v",
                    rr.Code, tt.wantStatus)
            }
        })
    }
}

Testing File Uploads

func TestFileUpload(t *testing.T) {
    // Create multipart form
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    
    // Add file to form
    part, err := writer.CreateFormFile("file", "test.txt")
    if err != nil {
        t.Fatal(err)
    }
    
    content := []byte("test content")
    if _, err := part.Write(content); err != nil {
        t.Fatal(err)
    }
    
    // Close writer
    writer.Close()
    
    // Create request
    req := httptest.NewRequest("POST", "/upload", body)
    req.Header.Set("Content-Type", writer.FormDataContentType())
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(UploadHandler)
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
}

Best Practices

1. Use Table-Driven Tests

func TestAPIEndpoint(t *testing.T) {
    tests := []struct {
        name       string
        method     string
        path       string
        body       io.Reader
        wantStatus int
        wantBody   string
    }{
        {
            name:       "get user",
            method:     "GET",
            path:       "/api/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `{"id":"123","name":"John"}`,
        },
        {
            name:       "user not found",
            method:     "GET",
            path:       "/api/users/999",
            wantStatus: http.StatusNotFound,
            wantBody:   `{"error":"user not found"}`,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(tt.method, tt.path, tt.body)
            rr := httptest.NewRecorder()
            
            handler := http.HandlerFunc(APIHandler)
            handler.ServeHTTP(rr, req)
            
            if rr.Code != tt.wantStatus {
                t.Errorf("status code = %d, want %d", rr.Code, tt.wantStatus)
            }
            
            if strings.TrimSpace(rr.Body.String()) != tt.wantBody {
                t.Errorf("body = %q, want %q", rr.Body.String(), tt.wantBody)
            }
        })
    }
}

2. Test Error Conditions

func TestErrorHandling(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusInternalServerError)
        fmt.Fprintln(w, `{"error": "internal server error"}`)
    }))
    defer server.Close()
    
    client := NewClient(server.URL)
    _, err := client.FetchData()
    
    if err == nil {
        t.Error("expected error, got nil")
    }
    
    if !strings.Contains(err.Error(), "internal server error") {
        t.Errorf("unexpected error message: %v", err)
    }
}

3. Clean Up Resources

func TestWithCleanup(t *testing.T) {
    // Create temporary file
    tmpFile, err := os.CreateTemp("", "test-*.txt")
    if err != nil {
        t.Fatal(err)
    }
    
    // Register cleanup
    t.Cleanup(func() {
        tmpFile.Close()
        os.Remove(tmpFile.Name())
    })
    
    // Create test server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, tmpFile.Name())
    }))
    t.Cleanup(server.Close)
    
    // Test code...
}

Common Patterns

Testing JSON APIs

func TestJSONAPI(t *testing.T) {
    payload := map[string]interface{}{
        "name": "John Doe",
        "age":  30,
    }
    
    jsonData, err := json.Marshal(payload)
    if err != nil {
        t.Fatal(err)
    }
    
    req := httptest.NewRequest("POST", "/api/users",
        bytes.NewBuffer(jsonData))
    req.Header.Set("Content-Type", "application/json")
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(CreateUserHandler)
    handler.ServeHTTP(rr, req)
    
    if rr.Code != http.StatusCreated {
        t.Errorf("status = %d, want %d", rr.Code, http.StatusCreated)
    }
    
    var response map[string]interface{}
    if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
        t.Fatal(err)
    }
    
    if id, ok := response["id"].(string); !ok || id == "" {
        t.Error("response missing id field")
    }
}

Next Steps