Backend Development 21 min read

Testing HTTP Applications in Go: Isolating External Dependencies

This article explains how to write reliable unit tests for Go HTTP applications by isolating external dependencies, covering server‑side handlers, client‑side monitoring, and using tools such as net/http/httptest, testify, and gock to create test doubles and mock HTTP services.

Go Programming World
Go Programming World
Go Programming World
Testing HTTP Applications in Go: Isolating External Dependencies

Ensuring the correctness of HTTP functionality is crucial for web applications, but unit testing becomes challenging when the code interacts with external services. The article shows how to isolate those dependencies in Go.

It starts with a simple Go HTTP server that provides two endpoints, POST /users and GET /users/:id , implemented by CreateUserHandler and GetUserHandler respectively:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "github.com/julienschmidt/httprouter"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var users = []User{{ID: 1, Name: "user1"}}

func CreateUserHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ... }
func GetUserHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ... }
func setupRouter() *httprouter.Router { ... }
func main() { router := setupRouter(); _ = http.ListenAndServe(":8000", router) }

The CreateUserHandler writes a JSON content‑type header, reads the request body, unmarshals it into a User struct, handles errors with appropriate status codes (400 for bad request, 500 for server error), appends the new user to the global users slice, and returns a 201 status code without a response body.

To unit‑test this handler, the article first identifies its external dependencies: the http.ResponseWriter , the *http.Request , the router parameters, and the global users slice. It introduces a helper setupTestUser that temporarily replaces the users slice with test data and returns a cleanup function:

func setupTestUser() func() {
    defaultUsers := users
    users = []User{{ID: 1, Name: "test-user1"}}
    return func() { users = defaultUsers }
}

Using the standard library net/http/httptest , the test creates a ResponseRecorder and a request, invokes the router, and asserts the response code, headers, and the state of the users slice with the testify assertion library:

func TestCreateUserHandler(t *testing.T) {
    cleanup := setupTestUser()
    defer cleanup()

    w := httptest.NewRecorder()
    body := strings.NewReader(`{"name": "user2"}`)
    req := httptest.NewRequest("POST", "/users", body)
    router := setupRouter()
    router.ServeHTTP(w, req)

    assert.Equal(t, 201, w.Code)
    assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
    assert.Equal(t, "", w.Body.String())
    assert.Equal(t, 2, len(users))
    u2, _ := json.Marshal(users[1])
    assert.Equal(t, `{"id":2,"name":"user2"}`, string(u2))
}

A similar table‑driven test is provided for GetUserHandler , checking both the successful retrieval of a user and the 404 response when the user does not exist.

For client‑side testing, the article presents a monitoring program that checks whether a process has exited and, if so, sends a notification to a Feishu webhook via the sendFeishu function. The external dependencies here are the environment variable WEBHOOK and the HTTP call inside sendFeishu .

To mock the webhook, a test HTTP server is created with httptest.NewServer :

func newTestServer() *httptest.Server {
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        switch r.RequestURI {
        case "/success":
            fmt.Fprintf(w, `{"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"}`)
        case "/error":
            fmt.Fprintf(w, `{"code":19001,"data":{},"msg":"param invalid: incoming webhook access token invalid"}`)
        }
    }))
}

func TestMain(m *testing.M) {
    ts = newTestServer()
    m.Run()
    ts.Close()
}

The corresponding test uses the server’s URL as the webhook and verifies the returned Result struct.

As an alternative to a real server, the article demonstrates mocking the HTTP request with the third‑party gock library. The mock intercepts POST requests to http://localhost:8080/webhook and returns a predefined JSON payload:

gock.New("http://localhost:8080").
    Post("/webhook").
    Reply(200).
    JSON(map[string]interface{}{
        "StatusCode":    0,
        "StatusMessage": "success",
        "Code":          0,
        "Data":          make(map[string]interface{}),
        "Msg":           "success",
    })

The test sets the WEBHOOK environment variable to the mocked URL, calls monitor , and asserts that the result matches the expected success structure and that all mocks have been satisfied.

In summary, the article shows how to write unit tests for Go HTTP servers and clients by replacing external dependencies with test doubles, using net/http/httptest , testify , and gock , and discusses two patterns for test setup and teardown: per‑test helpers with defer cleanup() and a global TestMain function.

testinggohttpunittesthttptestTestifygock
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.