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)避免不必要的日志输出。
要点总结
- httptest:使用
httptest.NewRequest和httptest.NewRecorder模拟请求 - 断言库:推荐
stretchr/testify/assert简化断言 - Handler 测试:模拟各种 HTTP 方法和参数
- 中间件测试:验证中间件拦截和传递逻辑
- Mock 数据:使用接口和 Mock 实现解耦数据库
- 表驱动测试:覆盖多种场景,结构清晰
📝 发现内容有误?点击此处直接编辑