Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
14 Aug 2025 · 5 min read ·Article 45 / 125
Go

45 Using `httptest` for End-to-End Testing

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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 httptest is 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 note
  • GET /notes : retrieves all notes

Here are the handlers:

go
 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(&note)
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:

go
 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.

go
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:

  1. Sends a POST /notes request with a JSON payload
  2. Sends a GET /notes request and verifies that the note we just created appears in the response
go
 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(&notesResp)
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:

MERMAID
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

Aspecthttptest Advantageshttptest Disadvantages
DependencyNo external server neededCannot test external infrastructure (e.g., a real DB connection)
SpeedSuper fast, in-memoryDoesn’t cover true E2E in production
RealismClose to real testing (full HTTP stack)Still in a test environment, not a real server
IsolationEasy to reset state, suitable for parallel testsSimple state, limited persistence

Simulating a Failure Case

What if we want to test an error, for example a malformed payload?

go
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:

go
1func createNoteHandler(w http.ResponseWriter, r *http.Request) {
2    var note Note
3    err := json.NewDecoder(r.Body).Decode(&note)
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

  1. Use httptest.NewServer for full handler tests, not just unit handler tests.
  2. Make sure state is always reset before each E2E test. Avoid side effects!
  3. Use a helper/setup function to reduce the boilerplate of setting up the test server.
  4. Use assertions from a library like testify for more readable checks.
  5. 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!

Related Articles

💬 Comments