单元测试

Gin 应用的单元测试主要使用 net/http/httptest 包来模拟 HTTP 请求。

基本测试

package main

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

func TestPingRoute(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/ping", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
    assert.JSONEq(t, `{"message":"pong"}`, w.Body.String())
}

测试 POST 请求

func TestCreateUser(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.POST("/users", func(c *gin.Context) {
        var user struct {
            Name  string `json:"name" binding:"required"`
            Email string `json:"email" binding:"required,email"`
        }
        
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(http.StatusCreated, gin.H{
            "id":    1,
            "name":  user.Name,
            "email": user.Email,
        })
    })
    
    body := `{"name":"张三","email":"zhangsan@example.com"}`
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusCreated, w.Code)
}

func TestCreateUserValidation(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.POST("/users", createUserHandler)
    
    body := `{"name":"","email":"invalid"}`
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusBadRequest, w.Code)
}

测试路由参数

func TestGetUser(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "id":   id,
            "name": "用户" + id,
        })
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/users/123", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
    assert.JSONEq(t, `{"id":"123","name":"用户123"}`, w.Body.String())
}

测试查询参数

func TestSearch(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.GET("/search", func(c *gin.Context) {
        q := c.Query("q")
        page := c.DefaultQuery("page", "1")
        
        c.JSON(http.StatusOK, gin.H{
            "query": q,
            "page":  page,
        })
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/search?q=golang&page=2", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
    assert.JSONEq(t, `{"query":"golang","page":"2"}`, w.Body.String())
}

测试中间件

func TestAuthMiddleware(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.Use(func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "未授权",
            })
            return
        }
        c.Next()
    })
    r.GET("/protected", func(c *gin.Context) {
        c.String(http.StatusOK, "ok")
    })
    
    t.Run("无token", func(t *testing.T) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/protected", nil)
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusUnauthorized, w.Code)
    })
    
    t.Run("有token", func(t *testing.T) {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/protected", nil)
        req.Header.Set("Authorization", "Bearer token")
        r.ServeHTTP(w, req)
        
        assert.Equal(t, http.StatusOK, w.Code)
    })
}

测试辅助函数

func performRequest(r http.Handler, method, path string, body io.Reader) *httptest.ResponseRecorder {
    req, _ := http.NewRequest(method, path, body)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    return w
}

func TestWithHelper(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.GET("/hello", func(c *gin.Context) {
        c.String(http.StatusOK, "hello")
    })
    
    w := performRequest(r, "GET", "/hello", nil)
    
    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, "hello", w.Body.String())
}

表格驱动测试

func TestCalculator(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.GET("/calc", func(c *gin.Context) {
        a, _ := strconv.Atoi(c.Query("a"))
        b, _ := strconv.Atoi(c.Query("b"))
        op := c.Query("op")
        
        var result int
        switch op {
        case "add":
            result = a + b
        case "sub":
            result = a - b
        case "mul":
            result = a * b
        }
        
        c.JSON(http.StatusOK, gin.H{"result": result})
    })
    
    tests := []struct {
        name     string
        query    string
        expected int
    }{
        {"加法", "a=1&b=2&op=add", 3},
        {"减法", "a=5&b=3&op=sub", 2},
        {"乘法", "a=4&b=3&op=mul", 12},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := performRequest(r, "GET", "/calc?"+tt.query, nil)
            
            var response map[string]int
            json.Unmarshal(w.Body.Bytes(), &response)
            
            assert.Equal(t, tt.expected, response["result"])
        })
    }
}

小结

Gin 的单元测试使用 httptest 包模拟 HTTP 请求。使用表格驱动测试可以覆盖多种场景。测试辅助函数可以减少重复代码。记得在测试中设置 gin.TestMode 避免不必要的日志输出。