45 Using `httptest` for End-to-End Testing
45 Using httptest for End-to-End Testing
Testing is an inseparable part of modern software development. One of its main challenges is ensuring that the entire application stack behaves as expected, especially for HTTP-based applications like the ones we commonly encounter in Go services. In this context, Go provides the powerful httptest package for testing applications from an end-to-end (E2E) perspective.
In this article, I’ll walk through the practical use of httptest for E2E testing in Go applications. The discussion will cover:
- What
httptestis and what it’s used for - A case study of a simple application
- The steps to build an E2E test
- Simulating request-response cycles
- Tips & best practices
- A comparison table
- A test flow diagram
Let’s get started!
What Is httptest?
httptest is an official package from Go’s standard library that provides utilities for HTTP testing. This package is typically used to:
- Create a test server (
httptest.NewServer) - Create a request and response recorder (
httptest.NewRecorder) - Test handlers and endpoints
With this package, we can simulate HTTP request/response cycles without having to run a full server outside the testing environment. It’s a great fit for both integration and E2E testing in Go applications.
Case Study: A Notes API Application
I’ll use a simple API application as an example — a Notes API — with two endpoints:
POST /notes: creates a new noteGET /notes: retrieves all notes
Here are the handlers:
1// note.go
2type Note struct {
3 ID int `json:"id"`
4 Body string `json:"body"`
5}
6
7var notes = []Note{}
8var currentID = 1
9
10func createNoteHandler(w http.ResponseWriter, r *http.Request) {
11 var note Note
12 json.NewDecoder(r.Body).Decode(¬e)
13 note.ID = currentID
14 currentID++
15 notes = append(notes, note)
16 w.WriteHeader(http.StatusCreated)
17 json.NewEncoder(w).Encode(note)
18}
19
20func getNotesHandler(w http.ResponseWriter, r *http.Request) {
21 w.Header().Set("Content-Type", "application/json")
22 json.NewEncoder(w).Encode(notes)
23}Simple routing using http.ServeMux:
1func NewRouter() http.Handler {
2 mux := http.NewServeMux()
3 mux.HandleFunc("/notes", func(w http.ResponseWriter, r *http.Request) {
4 if r.Method == http.MethodPost {
5 createNoteHandler(w, r)
6 } else if r.Method == http.MethodGet {
7 getNotesHandler(w, r)
8 } else {
9 http.NotFound(w, r)
10 }
11 })
12 return mux
13}Implementing End-to-End Tests with httptest
How do we use httptest to test this API from end to end? Here are the core steps:
1. Create a Test Server
Use httptest.NewServer to run the handler as a real HTTP server on a random port.
1func setupTestServer() *httptest.Server {
2 router := NewRouter()
3 server := httptest.NewServer(router)
4 return server
5}2. Test flow: Simulating an E2E Use Case
We’ll write a single integration test that:
- Sends a POST
/notesrequest with a JSON payload - Sends a GET
/notesrequest and verifies that the note we just created appears in the response
1func TestE2ENotesAPI(t *testing.T) {
2 server := setupTestServer()
3 defer server.Close()
4
5 // Step 1: Create a note
6 note := map[string]string{"body": "Belajar httptest"}
7 payload, _ := json.Marshal(note)
8 resp, err := http.Post(server.URL+"/notes",
9 "application/json", bytes.NewBuffer(payload))
10 require.NoError(t, err)
11 require.Equal(t, http.StatusCreated, resp.StatusCode)
12 var created Note
13 json.NewDecoder(resp.Body).Decode(&created)
14 resp.Body.Close()
15 assert.Equal(t, "Belajar httptest", created.Body)
16 assert.NotZero(t, created.ID)
17
18 // Step 2: Get all notes
19 resp, err = http.Get(server.URL + "/notes")
20 require.NoError(t, err)
21 require.Equal(t, http.StatusOK, resp.StatusCode)
22 var notesResp []Note
23 json.NewDecoder(resp.Body).Decode(¬esResp)
24 resp.Body.Close()
25 assert.Len(t, notesResp, 1)
26 assert.Equal(t, created, notesResp[0])
27}3. Test Flow Diagram
Let’s visualize the flow with a simple mermaid diagram:
sequenceDiagram
participant T as Test Code
participant S as httptest.Server
participant H as Handlers
T->>S: POST /notes (dengan payload)
S->>H: createNoteHandler
H-->>S: Response 201 (note baru)
S-->>T: Kirim response 201
T->>S: GET /notes
S->>H: getNotesHandler
H-->>S: Response 200 (daftar notes)
S-->>T: Kirim response 200 beserta notes
Table: Pros and Cons
| Aspect | httptest Advantages | httptest Disadvantages |
|---|---|---|
| Dependency | No external server needed | Cannot test external infrastructure (e.g., a real DB connection) |
| Speed | Super fast, in-memory | Doesn’t cover true E2E in production |
| Realism | Close to real testing (full HTTP stack) | Still in a test environment, not a real server |
| Isolation | Easy to reset state, suitable for parallel tests | Simple state, limited persistence |
Simulating a Failure Case
What if we want to test an error, for example a malformed payload?
1func TestCreateNote_MalformedPayload(t *testing.T) {
2 server := setupTestServer()
3 defer server.Close()
4
5 resp, err := http.Post(server.URL+"/notes",
6 "application/json", strings.NewReader("{broken_json}"))
7 require.NoError(t, err)
8 require.Equal(t, http.StatusBadRequest, resp.StatusCode)
9}Add validation to the handler:
1func createNoteHandler(w http.ResponseWriter, r *http.Request) {
2 var note Note
3 err := json.NewDecoder(r.Body).Decode(¬e)
4 if err != nil {
5 http.Error(w, "Invalid payload", http.StatusBadRequest)
6 return
7 }
8 // ... continue with the process as before
9}Tips and Best Practices
- Use
httptest.NewServerfor full handler tests, not just unit handler tests. - Make sure state is always reset before each E2E test. Avoid side effects!
- Use a helper/setup function to reduce the boilerplate of setting up the test server.
- Use assertions from a library like testify for more readable checks.
- Test edge-case variations (invalid payloads, unrecognized methods, and so on).
Conclusion
With httptest, Go makes it easy and fast to write E2E tests for HTTP APIs, without needing an external server. From simulating requests to checking responses, we can ensure our endpoints behave as expected before moving up to the production environment.
Is httptest automatically enough for every E2E need? Not always, especially if your application depends on several external services. However, for most Go API cases, httptest-based tests are a solid foundation and are highly recommended to adopt from day one of development.
Integrate it into your CI pipeline — and enjoy the confidence boost every time a new feature is shipped!
Also read:
Happy coding, and I hope this was helpful!