单元测试 #
一、测试概述 #
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