Testing Handlers
Learning Objectives
- Learn how to test HTTP handlers without starting a real server
- Understand how to use
httptest.NewRequest()to create test HTTP requests - Learn how to use
httptest.NewRecorder()to capture handler responses - Understand how to invoke handlers directly in tests
- Learn how to assert on HTTP status codes in tests
- Understand how to decode JSON responses in tests
- Practice the "want X; got Y" assertion pattern for clear test failures
Introduction
In this one, you're gonna write your first handler test. You'll test your listBooksHandler by creating a fake HTTP request, capturing the response, and asserting that it returns the correct status code and data. You'll use Go's httptest package to test handlers without starting a real server, making your tests fast and isolated.
Theory/Concept
Why test handlers?
Handlers are the core of your API - they process requests and return responses. Testing them ensures they work correctly with real database data, return the right status codes, and produce valid JSON. Without tests, you'd have to manually test every endpoint after each change, which is slow and error-prone.
Testing without a server:
Go's net/http/httptest package lets you test handlers without starting an HTTP server. Instead of making real HTTP requests over the network, you create test requests and response recorders that work entirely in memory. This makes tests fast, reliable, and easy to run.
Creating test requests:
req := httptest.NewRequest(http.MethodGet, "/books", nil)
httptest.NewRequest() creates an HTTP request for testing. It takes:
- The HTTP method (
http.MethodGet,http.MethodPost, etc.) - The URL path (
"/books") - An optional request body (
nilfor GET requests)
This creates a real *http.Request that your handler can't tell apart from a real network request. The third parameter is the request body - for GET requests, you pass nil since there's no body.
Capturing responses:
rr := httptest.NewRecorder()
httptest.NewRecorder() creates a response recorder that implements http.ResponseWriter. When your handler writes to it, the recorder captures:
- The status code (
rr.Code) - The response headers (
rr.Header()) - The response body (
rr.Body)
This lets you inspect everything your handler wrote without actually sending it over the network.
Invoking the handler:
app.listBooksHandler(rr, req)
You call your handler directly, passing the recorder and request. The handler runs exactly as it would in production - it queries the database, processes the data, and writes the response. The only difference is the response goes to the recorder instead of a network connection.
Asserting on status codes:
if rr.Code != http.StatusOK {
t.Errorf("want status code %d; got %d", http.StatusOK, rr.Code)
}
You check that the handler returned the expected status code. The "want X; got Y" pattern makes test failures clear - you immediately see what you expected versus what you got. t.Errorf() marks the test as failed but continues running, so you can check multiple things in one test.
Decoding JSON responses:
var resp bookResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatal(err)
}
You decode the response body into your expected struct type. json.NewDecoder() reads from rr.Body (which is a *bytes.Buffer) and decodes the JSON into your struct. If decoding fails, you use t.Fatal() to stop the test immediately - if the response isn't valid JSON, there's no point checking the data.
Asserting on response data:
booksCount := len(resp.Books)
if booksCount != 2 {
t.Errorf("want books count of 2; got %d", booksCount)
}
After decoding, you can assert on the actual data. Since your seed function inserts 2 books, you expect exactly 2 books in the response. You check the length of the slice and use the same "want X; got Y" pattern for clear failure messages.
The complete test pattern:
Your test follows a clear structure:
- Set up the test environment (database, app instance)
- Create a test request
- Create a response recorder
- Invoke the handler
- Assert on status code
- Decode the response
- Assert on response data
This pattern works for any handler test - you're testing the handler's behavior with real database data, not mocked dependencies.
Why this approach works:
- Fast - No network overhead, everything runs in memory
- Isolated - Each test gets a fresh in-memory database
- Realistic - Handlers run with real database queries
- Simple - No need to start/stop servers or manage ports
- Reliable - Tests don't depend on external services or network state
Key Takeaways
httptest.NewRequest()creates test HTTP requests without a real serverhttptest.NewRecorder()captures handler responses for inspection- You can invoke handlers directly in tests by calling them with a recorder and request
- Check status codes with
rr.Codeand use the "want X; got Y" pattern for clear failures - Decode JSON responses with
json.NewDecoder()to assert on response data - Use
t.Errorf()to mark tests as failed but continue,t.Fatal()to stop immediately - The test pattern: setup → create request → create recorder → invoke handler → assert
- Handler tests run fast and isolated because they don't use real network connections
Related Go Bytes
- Testing HTTP Handlers in Go - Video | GitHub Repo
- Go Testing Basics - Video | GitHub Repo
- JSON Decoding in Go - Video | GitHub Repo
0 comments