Go Testing HTTP Handlers — Testing HTTP Servers with httptest and Test Suites
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" >}} |