全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-18 10 分钟 ✍️ juanwangdev

Gin 单元测试编写

单元测试是保证代码质量的重要手段,Gin 框架提供了便捷的测试工具。

测试基础

测试文件结构

Go
// 文件命名:xxx_test.go
// 函数命名:func TestXxx(t *testing.T)

package main

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

func init() {
    gin.SetMode(gin.TestMode)
}

func TestPingHandler(t *testing.T) {
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })

    req := httptest.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.JSONEq(t, `{"message":"pong"}`, w.Body.String())
}

Handler 测试

GET 请求测试

Go
func GetUserHandler(c *gin.Context) {
    userID := c.Param("id")
    user := User{ID: userID, Name: "Test User"}
    c.JSON(200, user)
}

func TestGetUserHandler(t *testing.T) {
    r := gin.New()
    r.GET("/users/:id", GetUserHandler)

    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)

    var user User
    json.Unmarshal(w.Body.Bytes(), &user)
    assert.Equal(t, "123", user.ID)
    assert.Equal(t, "Test User", user.Name)
}

POST 请求测试

Go
func CreateUserHandler(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(201, gin.H{"id": "123", "name": user.Name})
}

func TestCreateUserHandler(t *testing.T) {
    r := gin.New()
    r.POST("/users", CreateUserHandler)

    body := `{"name":"New User","email":"test@example.com"}`
    req := httptest.NewRequest("POST", "/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 201, w.Code)
    assert.Contains(t, w.Body.String(), "New User")
}

func TestCreateUserHandlerInvalidBody(t *testing.T) {
    r := gin.New()
    r.POST("/users", CreateUserHandler)

    body := `{"name":""}`
    req := httptest.NewRequest("POST", "/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 400, w.Code)
}

Query 参数测试

Go
func ListUsersHandler(c *gin.Context) {
    page := c.DefaultQuery("page", "1")
    limit := c.DefaultQuery("limit", "10")
    c.JSON(200, gin.H{"page": page, "limit": limit})
}

func TestListUsersHandler(t *testing.T) {
    r := gin.New()
    r.GET("/users", ListUsersHandler)

    req := httptest.NewRequest("GET", "/users?page=2&limit=20", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.JSONEq(t, `{"page":"2","limit":"20"}`, w.Body.String())
}

func TestListUsersHandlerDefault(t *testing.T) {
    r := gin.New()
    r.GET("/users", ListUsersHandler)

    req := httptest.NewRequest("GET", "/users", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.JSONEq(t, `{"page":"1","limit":"10"}`, w.Body.String())
}

中间件测试

认证中间件测试

Go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(401, gin.H{"error": "未认证"})
            c.Abort()
            return
        }
        c.Set("user_id", "123")
        c.Next()
    }
}

func TestAuthMiddlewareSuccess(t *testing.T) {
    r := gin.New()
    r.Use(AuthMiddleware())
    r.GET("/protected", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "success"})
    })

    req := httptest.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", "valid-token")
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
}

func TestAuthMiddlewareFailure(t *testing.T) {
    r := gin.New()
    r.Use(AuthMiddleware())
    r.GET("/protected", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "success"})
    })

    req := httptest.NewRequest("GET", "/protected", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 401, w.Code)
    assert.Contains(t, w.Body.String(), "未认证")
}

请求 ID 中间件测试

Go
func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := uuid.New().String()
        c.Set("request_id", requestID)
        c.Header("X-Request-ID", requestID)
        c.Next()
    }
}

func TestRequestIDMiddleware(t *testing.T) {
    r := gin.New()
    r.Use(RequestIDMiddleware())
    r.GET("/test", func(c *gin.Context) {
        requestID := c.GetString("request_id")
        c.JSON(200, gin.H{"request_id": requestID})
    })

    req := httptest.NewRequest("GET", "/test", nil)
    w := httptest.NewRecorder()

    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.NotEmpty(t, w.Header().Get("X-Request-ID"))

    var response map[string]string
    json.Unmarshal(w.Body.Bytes(), &response)
    assert.NotEmpty(t, response["request_id"])
}

路由组测试

Go
func SetupRouter() *gin.Engine {
    r := gin.New()

    public := r.Group("/api")
    {
        public.GET("/health", HealthHandler)
    }

    protected := r.Group("/api")
    protected.Use(AuthMiddleware())
    {
        protected.GET("/users", ListUsersHandler)
        protected.POST("/users", CreateUserHandler)
    }

    return r
}

func TestProtectedRoutes(t *testing.T) {
    r := SetupRouter()

    // 无 token 访问
    req := httptest.NewRequest("GET", "/api/users", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    assert.Equal(t, 401, w.Code)

    // 有 token 访问
    req = httptest.NewRequest("GET", "/api/users", nil)
    req.Header.Set("Authorization", "token")
    w = httptest.NewRecorder()
    r.ServeHTTP(w, req)
    assert.Equal(t, 200, w.Code)
}

func TestPublicRoutes(t *testing.T) {
    r := SetupRouter()

    req := httptest.NewRequest("GET", "/api/health", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    assert.Equal(t, 200, w.Code)
}

表驱动测试

Go
func TestValidation(t *testing.T) {
    tests := []struct {
        name     string
        input    User
        wantCode int
    }{
        {
            name:     "valid user",
            input:    User{Name: "John", Email: "john@example.com"},
            wantCode: 201,
        },
        {
            name:     "empty name",
            input:    User{Name: "", Email: "john@example.com"},
            wantCode: 400,
        },
        {
            name:     "invalid email",
            input:    User{Name: "John", Email: "invalid"},
            wantCode: 400,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            r := gin.New()
            r.POST("/users", CreateUserHandler)

            body, _ := json.Marshal(tt.input)
            req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            r.ServeHTTP(w, req)

            assert.Equal(t, tt.wantCode, w.Code)
        })
    }
}

Mock 数据库

使用接口 Mock

Go
type UserRepository interface {
    FindByID(id string) (*User, error)
    Create(user *User) error
}

type MockUserRepository struct {
    users map[string]*User
}

func (m *MockUserRepository) FindByID(id string) (*User, error) {
    if user, exists := m.users[id]; exists {
        return user, nil
    }
    return nil, errors.New("user not found")
}

func (m *MockUserRepository) Create(user *User) error {
    m.users[user.ID] = user
    return nil
}

func GetUserHandler(repo UserRepository) gin.HandlerFunc {
    return func(c *gin.Context) {
        id := c.Param("id")
        user, err := repo.FindByID(id)
        if err != nil {
            c.JSON(404, gin.H{"error": "用户不存在"})
            return
        }
        c.JSON(200, user)
    }
}

func TestGetUserHandlerWithMock(t *testing.T) {
    mockRepo := &MockUserRepository{
        users: map[string]*User{
            "123": &User{ID: "123", Name: "Test User"},
        },
    }

    r := gin.New()
    r.GET("/users/:id", GetUserHandler(mockRepo))

    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)

    req = httptest.NewRequest("GET", "/users/999", nil)
    w = httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, 404, w.Code)
}

测试辅助函数

Go
// 创建测试请求
func createTestRequest(method, path string, body interface{}) *http.Request {
    var bodyReader io.Reader
    if body != nil {
        jsonBody, _ := json.Marshal(body)
        bodyReader = bytes.NewReader(jsonBody)
    }

    req := httptest.NewRequest(method, path, bodyReader)
    if body != nil {
        req.Header.Set("Content-Type", "application/json")
    }
    return req
}

// 执行请求并返回响应
func performRequest(r *gin.Engine, req *http.Request) *httptest.ResponseRecorder {
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    return w
}

func TestWithHelpers(t *testing.T) {
    r := gin.New()
    r.POST("/users", CreateUserHandler)

    req := createTestRequest("POST", "/users", User{Name: "Test"})
    w := performRequest(r, req)

    assert.Equal(t, 201, w.Code)
}

测试运行

Bash
# 运行所有测试
go test ./...

# 运行特定测试
go test -run TestPingHandler

# 详细输出
go test -v ./...

# 测试覆盖率
go test -cover ./...

# 生成覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

测试结构规范

规范说明
文件命名xxx_test.go
函数命名func TestXxx(t *testing.T)
包名与被测试代码同包
子测试t.Run("name", func(t *testing.T){})

注意:测试前设置 gin.SetMode(gin.TestMode) 避免不必要的日志输出。

要点总结

  1. httptest:使用 httptest.NewRequesthttptest.NewRecorder 模拟请求
  2. 断言库:推荐 stretchr/testify/assert 简化断言
  3. Handler 测试:模拟各种 HTTP 方法和参数
  4. 中间件测试:验证中间件拦截和传递逻辑
  5. Mock 数据:使用接口和 Mock 实现解耦数据库
  6. 表驱动测试:覆盖多种场景,结构清晰

📝 发现内容有误?点击此处直接编辑

← 上一篇 Gin pprof性能分析
下一篇 → Gin 压力测试工具使用
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库