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
- Learn about Integration Tests
- Explore Test Coverage
- Study Mocking