单元测试 #

一、测试概述 #

1.1 测试类型 #

类型 说明
单元测试 测试单个函数或方法
集成测试 测试多个组件协作
端到端测试 测试完整流程

1.2 测试原则 #

text
1. 测试代码应该简单明了
2. 测试应该独立运行
3. 测试应该可重复
4. 测试应该快速执行

二、基本测试 #

2.1 测试文件命名 #

text
main.go        → main_test.go
handler.go     → handler_test.go
user.go        → user_test.go

2.2 测试函数命名 #

go
func TestFunctionName(t *testing.T) {}
func TestFunctionName_Scenario(t *testing.T) {}
func TestFunctionName_Input_Expected(t *testing.T) {}

2.3 基本测试示例 #

go
package main

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

func TestPingHandler(t *testing.T) {
    // 设置Gin为测试模式
    gin.SetMode(gin.TestMode)
    
    // 创建路由
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    
    // 创建请求
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/ping", nil)
    
    // 执行请求
    r.ServeHTTP(w, req)
    
    // 验证结果
    assert.Equal(t, 200, w.Code)
    assert.JSONEq(t, `{"message": "pong"}`, w.Body.String())
}

三、测试HTTP处理函数 #

3.1 测试GET请求 #

go
func TestGetUser(t *testing.T) {
    r := gin.New()
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(200, gin.H{"id": id, "name": "Alice"})
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/users/1", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "Alice")
}

3.2 测试POST请求 #

go
func TestCreateUser(t *testing.T) {
    r := gin.New()
    r.POST("/users", func(c *gin.Context) {
        var user struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
        
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(201, gin.H{
            "id":    1,
            "name":  user.Name,
            "email": user.Email,
        })
    })
    
    body := `{"name": "Alice", "email": "alice@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, 201, w.Code)
}

3.3 测试中间件 #

go
func TestAuthMiddleware(t *testing.T) {
    r := gin.New()
    r.Use(func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
            return
        }
        c.Next()
    })
    r.GET("/protected", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "ok"})
    })
    
    // 测试无Token
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/protected", nil)
    r.ServeHTTP(w, req)
    assert.Equal(t, 401, w.Code)
    
    // 测试有Token
    w = httptest.NewRecorder()
    req, _ = http.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", "Bearer token")
    r.ServeHTTP(w, req)
    assert.Equal(t, 200, w.Code)
}

四、Mock测试 #

4.1 Mock数据库 #

go
type MockDB struct {
    users map[uint]*User
}

func NewMockDB() *MockDB {
    return &MockDB{
        users: make(map[uint]*User),
    }
}

func (m *MockDB) Find(dest interface{}, conds ...interface{}) *gorm.DB {
    // 实现Mock逻辑
    return &gorm.DB{}
}

func TestWithMockDB(t *testing.T) {
    mockDB := NewMockDB()
    mockDB.users[1] = &User{ID: 1, Name: "Alice"}
    
    r := gin.New()
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        // 使用mockDB查询
        c.JSON(200, gin.H{"id": id})
    })
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/users/1", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, 200, w.Code)
}

4.2 Mock接口 #

go
type UserRepository interface {
    FindByID(id uint) (*User, error)
    Create(user *User) error
}

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

func (m *MockUserRepository) FindByID(id uint) (*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 TestUserService(t *testing.T) {
    repo := &MockUserRepository{
        users: make(map[uint]*User),
    }
    
    user := &User{ID: 1, Name: "Alice"}
    err := repo.Create(user)
    
    assert.NoError(t, err)
    
    found, err := repo.FindByID(1)
    assert.NoError(t, err)
    assert.Equal(t, "Alice", found.Name)
}

五、测试覆盖率 #

5.1 生成覆盖率报告 #

bash
# 运行测试并生成覆盖率
go test -cover ./...

# 生成详细覆盖率报告
go test -coverprofile=coverage.out ./...

# 查看覆盖率
go tool cover -func=coverage.out

# 生成HTML报告
go tool cover -html=coverage.out -o coverage.html

5.2 覆盖率目标 #

text
单元测试覆盖率目标:
- 核心业务逻辑:80%+
- 工具函数:90%+
- HTTP处理函数:70%+

六、测试最佳实践 #

6.1 表格驱动测试 #

go
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -1, 1, 0},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            assert.Equal(t, tt.expected, result)
        })
    }
}

6.2 测试辅助函数 #

go
func setupTestRouter() *gin.Engine {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    return r
}

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

func TestWithHelper(t *testing.T) {
    r := setupTestRouter()
    r.GET("/test", func(c *gin.Context) {
        c.String(200, "ok")
    })
    
    w := makeRequest(r, "GET", "/test", nil)
    assert.Equal(t, 200, w.Code)
}

6.3 测试清理 #

go
func TestWithCleanup(t *testing.T) {
    // 设置
    db := setupTestDB()
    
    // 清理
    t.Cleanup(func() {
        db.Close()
    })
    
    // 测试代码
}

七、总结 #

7.1 核心要点 #

要点 说明
httptest 测试HTTP处理函数
assert 断言库
Mock 模拟依赖
覆盖率 测试覆盖率报告

7.2 最佳实践 #

实践 说明
表格驱动 使用表格驱动测试
辅助函数 封装测试辅助函数
清理资源 使用t.Cleanup
独立测试 测试之间不依赖

7.3 下一步 #

现在你已经掌握了单元测试,接下来让我们学习 API测试,了解API集成测试!

最后更新:2026-03-28