Backend Development 13 min read

Master HTTP Mocking in Go: httptest & gock Tutorial

This article explains how to use Go's httptest package and the gock library to mock HTTP servers and external API calls, providing step‑by‑step code examples, test case design, and practical tips for reliable backend unit testing.

Raymond Ops
Raymond Ops
Raymond Ops
Master HTTP Mocking in Go: httptest & gock Tutorial

1. httptest

1.1 Preparation

Assume our business logic is to build an HTTP server that handles user login requests, requiring an email and a password.

<code>package main

import (
    regexp "github.com/dlclark/regexp2"
    "github.com/gin-gonic/gin"
    "net/http"
)

type UserHandler struct {
    emailExp    *regexp.Regexp
    passwordExp *regexp.Regexp
}

func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
    ug := server.Group("/user")
    ug.POST("/login", u.Login)
}

func NewUserHandler() *UserHandler {
    const (
        emailRegexPattern    = "^\\w+([-+. ]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
        passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
    )
    emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
    passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
    return &UserHandler{emailExp: emailExp, passwordExp: passwordExp}
}

type LoginRequest struct {
    Email string `json:"email"`
    Pwd   string `json:"pwd"`
}

func (u *UserHandler) Login(ctx *gin.Context) {
    var req LoginRequest
    if err := ctx.ShouldBindJSON(&req); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"msg": "参数不正确!"})
        return
    }
    if req.Email == "" || req.Pwd == "" {
        ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不能为空"})
        return
    }
    ok, err := u.emailExp.MatchString(req.Email)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
        return
    }
    if !ok {
        ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱格式不正确"})
        return
    }
    ok, err = u.passwordExp.MatchString(req.Pwd)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
        return
    }
    if !ok {
        ctx.JSON(http.StatusBadRequest, gin.H{"msg": "密码必须大于8位,包含数字、特殊字符"})
        return
    }
    if req.Email != "[email protected]" || req.Pwd != "hello#world123" {
        ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不匹配!"})
        return
    }
    ctx.JSON(http.StatusOK, gin.H{"msg": "登录成功!"})
}

func InitWebServer(userHandler *UserHandler) *gin.Engine {
    server := gin.Default()
    userHandler.RegisterRoutes(server)
    return server
}

func main() {
    uh := &UserHandler{}
    server := InitWebServer(uh)
    server.Run(":8080") // 在8080端口启动服务器
}
</code>

1.2 Introduction

In web‑development scenarios, unit tests often need to simulate HTTP requests and responses. The Go standard library

net/http/httptest

lets you create an HTTP server instance inside test code, define specific request/response behavior, and thus mock real network interactions.

1.3 Basic usage

Typical steps for using

httptest

:

Import the

net/http/httptest

package.

Create an

httptest.Server

and specify the desired server behavior.

In test code, use

httptest.NewRequest

to craft a mock HTTP request and send it to the

httptest.Server

.

Check the response content or status code against expectations.

Simple example:

<code>package main

import (
    "bytes"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserHandler_Login(t *testing.T) {
    testCases := []struct {
        name     string
        reqBody  string
        wantCode int
        wantBody string
    }{
        {"登录成功", `{"email":"[email protected]","pwd":"hello#world123"}`, http.StatusOK, `{"msg":"登录成功!"}`},
        {"参数不正确", `{"email":"[email protected]","pwd":"hello#world123",}`, http.StatusBadRequest, `{"msg":"参数不正确!"}`},
        {"邮箱或密码为空", `{"email":"","pwd":""}`, http.StatusBadRequest, `{"msg":"邮箱或密码不能为空"}`},
        {"邮箱格式不正确", `{"email":"invalidemail","pwd":"hello#world123"}`, http.StatusBadRequest, `{"msg":"邮箱格式不正确"}`},
        {"密码格式不正确", `{"email":"[email protected]","pwd":"invalidpassword"}`, http.StatusBadRequest, `{"msg":"密码必须大于8位,包含数字、特殊字符"}`},
        {"邮箱或密码不匹配", `{"email":"[email protected]","pwd":"hello#world123"}`, http.StatusBadRequest, `{"msg":"邮箱或密码不匹配!"}`},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            server := gin.Default()
            h := NewUserHandler()
            h.RegisterRoutes(server)
            req, err := http.NewRequest(http.MethodPost, "/user/login", bytes.NewBuffer([]byte(tc.reqBody)))
            assert.NoError(t, err)
            req.Header.Set("Content-Type", "application/json")
            resp := httptest.NewRecorder()
            server.ServeHTTP(resp, req)
            assert.Equal(t, tc.wantCode, resp.Code)
            assert.JSONEq(t, tc.wantBody, resp.Body.String())
        })
    }
}
</code>

The test creates a Gin context, registers routes, crafts a mock request, records the response with

httptest.NewRecorder()

, and uses assertions to verify status codes and JSON bodies.

图片
图片

2. gock

2.1 Introduction

gock helps you mock HTTP requests and responses during testing, which is especially useful for code that calls external APIs. It lets you define mock expectations and verify that your application handles them correctly.

2.2 Installation

<code>go get -u github.com/h2non/gock</code>

2.3 Basic usage

Typical workflow:

Start interceptor : call

gock.New

with the target host before the test begins.

Define rules : use methods such as

gock.Intercept

,

MatchType

,

JSON

, etc., to specify URL, method, headers, and request body expectations.

Set responses : use

gock.NewJson

,

gock.NewText

, or

Reply

to define the mock response payload and status code.

Run test : execute your code; gock will intercept matching HTTP calls and return the predefined responses.

2.4 Example

2.4.1 Preparation

Suppose we have a function that calls an external API at

http://your-api.com/post

and processes the JSON result.

<code>// ReqParam API request parameters
type ReqParam struct {
    X int `json:"x"`
}

// Result API response structure
type Result struct {
    Value int `json:"value"`
}

func GetResultByAPI(x, y int) int {
    p := &ReqParam{X: x}
    b, _ := json.Marshal(p)
    resp, err := http.Post("http://your-api.com/post", "application/json", bytes.NewBuffer(b))
    if err != nil {
        return -1
    }
    body, _ := ioutil.ReadAll(resp.Body)
    var ret Result
    if err := json.Unmarshal(body, &ret); err != nil {
        return -1
    }
    return ret.Value + y
}
</code>

2.4.2 Test case

Use gock to mock the external API for two different input values and assert the computed results.

<code>package gock_demo

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "gopkg.in/h2non/gock.v1"
)

func TestGetResultByAPI(t *testing.T) {
    defer gock.Off() // clean up after test

    // Mock request with x=1 returning value 100
    gock.New("http://your-api.com").
        Post("/post").
        MatchType("json").
        JSON(map[string]int{"x": 1}).
        Reply(200).
        JSON(map[string]int{"value": 100})
    res := GetResultByAPI(1, 1)
    assert.Equal(t, 101, res)

    // Mock request with x=2 returning value 200
    gock.New("http://your-api.com").
        Post("/post").
        MatchType("json").
        JSON(map[string]int{"x": 2}).
        Reply(200).
        JSON(map[string]int{"value": 200})
    res = GetResultByAPI(2, 2)
    assert.Equal(t, 202, res)

    assert.True(t, gock.IsDone()) // ensure all mocks were triggered
}
</code>
Backend DevelopmentGounit testingmockinghttptestgock
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

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.