3.5.4 API 测试与调试 #
API 测试是确保 API 质量和可靠性的关键环节。本节将详细介绍各种 API 测试方法、工具和调试技巧,帮助你构建健壮的 API 服务。
单元测试 #
单元测试是 API 测试的基础,主要测试单个函数或方法的功能。
基础单元测试 #
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/gorm"
)
// 测试套件
type APITestSuite struct {
suite.Suite
router *gin.Engine
db *gorm.DB
}
// 设置测试环境
func (suite *APITestSuite) SetupSuite() {
// 设置 Gin 为测试模式
gin.SetMode(gin.TestMode)
// 初始化测试数据库
suite.db = setupTestDB()
// 初始化路由
suite.router = setupTestRouter(suite.db)
}
// 清理测试环境
func (suite *APITestSuite) TearDownSuite() {
cleanupTestDB(suite.db)
}
// 每个测试前的准备
func (suite *APITestSuite) SetupTest() {
// 清理测试数据
suite.db.Exec("DELETE FROM users")
// 插入测试数据
testUsers := []User{
{ID: 1, Username: "testuser1", Email: "[email protected]", FullName: "Test User 1", IsActive: true},
{ID: 2, Username: "testuser2", Email: "[email protected]", FullName: "Test User 2", IsActive: true},
{ID: 3, Username: "testuser3", Email: "[email protected]", FullName: "Test User 3", IsActive: false},
}
for _, user := range testUsers {
suite.db.Create(&user)
}
}
// 测试获取用户列表
func (suite *APITestSuite) TestGetUsers() {
tests := []struct {
name string
queryParams string
expectedStatus int
expectedCount int
checkResponse func(*testing.T, *httptest.ResponseRecorder)
}{
{
name: "获取所有用户",
queryParams: "",
expectedStatus: http.StatusOK,
expectedCount: 3,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var response UsersListResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response.Users, 3)
assert.Equal(t, 3, response.Meta.Total)
},
},
{
name: "分页测试",
queryParams: "?page=1&per_page=2",
expectedStatus: http.StatusOK,
expectedCount: 2,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var response UsersListResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response.Users, 2)
assert.Equal(t, 1, response.Meta.Page)
assert.Equal(t, 2, response.Meta.PerPage)
assert.True(t, response.Meta.HasNext)
assert.False(t, response.Meta.HasPrev)
},
},
{
name: "搜索测试",
queryParams: "?search=testuser1",
expectedStatus: http.StatusOK,
expectedCount: 1,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var response UsersListResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response.Users, 1)
assert.Equal(t, "testuser1", response.Users[0].Username)
},
},
{
name: "状态过滤测试",
queryParams: "?is_active=true",
expectedStatus: http.StatusOK,
expectedCount: 2,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var response UsersListResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response.Users, 2)
for _, user := range response.Users {
assert.True(t, user.IsActive)
}
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
req := httptest.NewRequest("GET", "/api/v1/users"+tt.queryParams, nil)
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
suite.router.ServeHTTP(w, req)
assert.Equal(suite.T(), tt.expectedStatus, w.Code)
if tt.checkResponse != nil {
tt.checkResponse(suite.T(), w)
}
})
}
}
// 测试获取单个用户
func (suite *APITestSuite) TestGetUser() {
tests := []struct {
name string
userID string
expectedStatus int
checkResponse func(*testing.T, *httptest.ResponseRecorder)
}{
{
name: "获取存在的用户",
userID: "1",
expectedStatus: http.StatusOK,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var user User
err := json.Unmarshal(w.Body.Bytes(), &user)
assert.NoError(t, err)
assert.Equal(t, uint(1), user.ID)
assert.Equal(t, "testuser1", user.Username)
},
},
{
name: "获取不存在的用户",
userID: "999",
expectedStatus: http.StatusNotFound,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "User not found", errorResp.Error)
},
},
{
name: "无效的用户ID",
userID: "invalid",
expectedStatus: http.StatusBadRequest,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "Invalid ID", errorResp.Error)
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
req := httptest.NewRequest("GET", "/api/v1/users/"+tt.userID, nil)
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
suite.router.ServeHTTP(w, req)
assert.Equal(suite.T(), tt.expectedStatus, w.Code)
if tt.checkResponse != nil {
tt.checkResponse(suite.T(), w)
}
})
}
}
// 测试创建用户
func (suite *APITestSuite) TestCreateUser() {
tests := []struct {
name string
requestBody interface{}
expectedStatus int
checkResponse func(*testing.T, *httptest.ResponseRecorder)
}{
{
name: "创建有效用户",
requestBody: CreateUserRequest{
Username: "newuser",
Email: "[email protected]",
FullName: "New User",
Password: "password123",
},
expectedStatus: http.StatusCreated,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var user User
err := json.Unmarshal(w.Body.Bytes(), &user)
assert.NoError(t, err)
assert.Equal(t, "newuser", user.Username)
assert.Equal(t, "[email protected]", user.Email)
assert.True(t, user.IsActive)
// 检查 Location 头
location := w.Header().Get("Location")
assert.Contains(t, location, "/api/v1/users/")
},
},
{
name: "用户名已存在",
requestBody: CreateUserRequest{
Username: "testuser1", // 已存在的用户名
Email: "[email protected]",
FullName: "Different User",
Password: "password123",
},
expectedStatus: http.StatusConflict,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "User already exists", errorResp.Error)
},
},
{
name: "邮箱已存在",
requestBody: CreateUserRequest{
Username: "differentuser",
Email: "[email protected]", // 已存在的邮箱
FullName: "Different User",
Password: "password123",
},
expectedStatus: http.StatusConflict,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "User already exists", errorResp.Error)
},
},
{
name: "验证失败 - 缺少必需字段",
requestBody: CreateUserRequest{
Username: "", // 空用户名
Email: "invalid-email", // 无效邮箱
FullName: "",
Password: "123", // 密码太短
},
expectedStatus: http.StatusBadRequest,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "Validation failed", errorResp.Error)
assert.NotEmpty(t, errorResp.Details)
},
},
{
name: "无效的 JSON",
requestBody: "invalid json",
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
var body []byte
var err error
if str, ok := tt.requestBody.(string); ok {
body = []byte(str)
} else {
body, err = json.Marshal(tt.requestBody)
assert.NoError(suite.T(), err)
}
req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
suite.router.ServeHTTP(w, req)
assert.Equal(suite.T(), tt.expectedStatus, w.Code)
if tt.checkResponse != nil {
tt.checkResponse(suite.T(), w)
}
})
}
}
// 测试更新用户
func (suite *APITestSuite) TestUpdateUser() {
tests := []struct {
name string
userID string
requestBody interface{}
expectedStatus int
checkResponse func(*testing.T, *httptest.ResponseRecorder)
}{
{
name: "更新用户信息",
userID: "1",
requestBody: UpdateUserRequest{
Email: stringPtr("[email protected]"),
FullName: stringPtr("Updated Name"),
IsActive: boolPtr(false),
},
expectedStatus: http.StatusOK,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var user User
err := json.Unmarshal(w.Body.Bytes(), &user)
assert.NoError(t, err)
assert.Equal(t, "[email protected]", user.Email)
assert.Equal(t, "Updated Name", user.FullName)
assert.False(t, user.IsActive)
},
},
{
name: "部分更新",
userID: "2",
requestBody: UpdateUserRequest{
FullName: stringPtr("Partially Updated"),
},
expectedStatus: http.StatusOK,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var user User
err := json.Unmarshal(w.Body.Bytes(), &user)
assert.NoError(t, err)
assert.Equal(t, "Partially Updated", user.FullName)
assert.Equal(t, "[email protected]", user.Email) // 未更改
},
},
{
name: "邮箱冲突",
userID: "1",
requestBody: UpdateUserRequest{
Email: stringPtr("[email protected]"), // 已被用户2使用
},
expectedStatus: http.StatusConflict,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var errorResp ErrorResponse
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "Email already taken", errorResp.Error)
},
},
{
name: "用户不存在",
userID: "999",
requestBody: UpdateUserRequest{FullName: stringPtr("Test")},
expectedStatus: http.StatusNotFound,
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
body, err := json.Marshal(tt.requestBody)
assert.NoError(suite.T(), err)
req := httptest.NewRequest("PUT", "/api/v1/users/"+tt.userID, bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
suite.router.ServeHTTP(w, req)
assert.Equal(suite.T(), tt.expectedStatus, w.Code)
if tt.checkResponse != nil {
tt.checkResponse(suite.T(), w)
}
})
}
}
// 测试删除用户
func (suite *APITestSuite) TestDeleteUser() {
tests := []struct {
name string
userID string
expectedStatus int
checkResponse func(*testing.T, *httptest.ResponseRecorder)
}{
{
name: "删除存在的用户",
userID: "1",
expectedStatus: http.StatusNoContent,
checkResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
// 验证用户已被删除
var count int64
suite.db.Model(&User{}).Where("id = ?", 1).Count(&count)
assert.Equal(t, int64(0), count)
},
},
{
name: "删除不存在的用户",
userID: "999",
expectedStatus: http.StatusNotFound,
},
{
name: "无效的用户ID",
userID: "invalid",
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
req := httptest.NewRequest("DELETE", "/api/v1/users/"+tt.userID, nil)
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
suite.router.ServeHTTP(w, req)
assert.Equal(suite.T(), tt.expectedStatus, w.Code)
if tt.checkResponse != nil {
tt.checkResponse(suite.T(), w)
}
})
}
}
// 运行测试套件
func TestAPITestSuite(t *testing.T) {
suite.Run(t, new(APITestSuite))
}
// 辅助函数
func stringPtr(s string) *string {
return &s
}
func boolPtr(b bool) *bool {
return &b
}
func generateTestToken() string {
// 生成测试用的 JWT 令牌
return "test-jwt-token"
}
集成测试 #
集成测试验证多个组件之间的交互。
HTTP 集成测试 #
package integration
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type IntegrationTestSuite struct {
suite.Suite
baseURL string
client *http.Client
token string
}
func (suite *IntegrationTestSuite) SetupSuite() {
suite.baseURL = "http://localhost:8080"
suite.client = &http.Client{
Timeout: 30 * time.Second,
}
// 获取认证令牌
suite.token = suite.getAuthToken()
}
func (suite *IntegrationTestSuite) getAuthToken() string {
loginReq := LoginRequest{
Username: "admin",
Password: "admin123",
}
body, _ := json.Marshal(loginReq)
resp, err := suite.client.Post(
suite.baseURL+"/api/v1/auth/login",
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
suite.T().Fatalf("Failed to login: %v", err)
}
defer resp.Body.Close()
var loginResp LoginResponse
json.NewDecoder(resp.Body).Decode(&loginResp)
return loginResp.Token
}
func (suite *IntegrationTestSuite) makeRequest(method, path string, body interface{}) (*http.Response, error) {
var reqBody []byte
var err error
if body != nil {
reqBody, err = json.Marshal(body)
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(method, suite.baseURL+path, bytes.NewBuffer(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+suite.token)
return suite.client.Do(req)
}
func (suite *IntegrationTestSuite) TestUserCRUDFlow() {
// 1. 创建用户
createReq := CreateUserRequest{
Username: "integrationtest",
Email: "[email protected]",
FullName: "Integration Test User",
Password: "password123",
}
resp, err := suite.makeRequest("POST", "/api/v1/users", createReq)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
var createdUser User
json.NewDecoder(resp.Body).Decode(&createdUser)
resp.Body.Close()
userID := createdUser.ID
assert.NotZero(suite.T(), userID)
assert.Equal(suite.T(), "integrationtest", createdUser.Username)
// 2. 获取创建的用户
resp, err = suite.makeRequest("GET", fmt.Sprintf("/api/v1/users/%d", userID), nil)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var fetchedUser User
json.NewDecoder(resp.Body).Decode(&fetchedUser)
resp.Body.Close()
assert.Equal(suite.T(), userID, fetchedUser.ID)
assert.Equal(suite.T(), "integrationtest", fetchedUser.Username)
// 3. 更新用户
updateReq := UpdateUserRequest{
FullName: stringPtr("Updated Integration Test User"),
IsActive: boolPtr(false),
}
resp, err = suite.makeRequest("PUT", fmt.Sprintf("/api/v1/users/%d", userID), updateReq)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var updatedUser User
json.NewDecoder(resp.Body).Decode(&updatedUser)
resp.Body.Close()
assert.Equal(suite.T(), "Updated Integration Test User", updatedUser.FullName)
assert.False(suite.T(), updatedUser.IsActive)
// 4. 验证用户列表中包含更新的用户
resp, err = suite.makeRequest("GET", "/api/v1/users?search=integrationtest", nil)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var usersResp UsersListResponse
json.NewDecoder(resp.Body).Decode(&usersResp)
resp.Body.Close()
assert.Len(suite.T(), usersResp.Users, 1)
assert.Equal(suite.T(), userID, usersResp.Users[0].ID)
// 5. 删除用户
resp, err = suite.makeRequest("DELETE", fmt.Sprintf("/api/v1/users/%d", userID), nil)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusNoContent, resp.StatusCode)
resp.Body.Close()
// 6. 验证用户已被删除
resp, err = suite.makeRequest("GET", fmt.Sprintf("/api/v1/users/%d", userID), nil)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusNotFound, resp.StatusCode)
resp.Body.Close()
}
func (suite *IntegrationTestSuite) TestAuthenticationFlow() {
// 1. 注册新用户
registerReq := CreateUserRequest{
Username: "authtest",
Email: "[email protected]",
FullName: "Auth Test User",
Password: "password123",
}
resp, err := suite.makeRequest("POST", "/api/v1/auth/register", registerReq)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
resp.Body.Close()
// 2. 登录
loginReq := LoginRequest{
Username: "authtest",
Password: "password123",
}
body, _ := json.Marshal(loginReq)
resp, err = suite.client.Post(
suite.baseURL+"/api/v1/auth/login",
"application/json",
bytes.NewBuffer(body),
)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var loginResp LoginResponse
json.NewDecoder(resp.Body).Decode(&loginResp)
resp.Body.Close()
assert.NotEmpty(suite.T(), loginResp.Token)
assert.NotEmpty(suite.T(), loginResp.RefreshToken)
assert.Equal(suite.T(), "authtest", loginResp.User.Username)
// 3. 使用令牌访问受保护的端点
req, _ := http.NewRequest("GET", suite.baseURL+"/api/v1/users", nil)
req.Header.Set("Authorization", "Bearer "+loginResp.Token)
resp, err = suite.client.Do(req)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
resp.Body.Close()
// 4. 刷新令牌
refreshReq := map[string]string{
"refresh_token": loginResp.RefreshToken,
}
body, _ = json.Marshal(refreshReq)
resp, err = suite.client.Post(
suite.baseURL+"/api/v1/auth/refresh",
"application/json",
bytes.NewBuffer(body),
)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var refreshResp LoginResponse
json.NewDecoder(resp.Body).Decode(&refreshResp)
resp.Body.Close()
assert.NotEmpty(suite.T(), refreshResp.Token)
assert.NotEqual(suite.T(), loginResp.Token, refreshResp.Token)
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
性能测试 #
性能测试确保 API 在高负载下的表现。
基准测试 #
package benchmark
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func BenchmarkGetUsers(b *testing.B) {
router := setupTestRouter()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("GET", "/api/v1/users", nil)
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", w.Code)
}
}
})
}
func BenchmarkCreateUser(b *testing.B) {
router := setupTestRouter()
createReq := CreateUserRequest{
Username: "benchuser",
Email: "[email protected]",
FullName: "Benchmark User",
Password: "password123",
}
body, _ := json.Marshal(createReq)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
// 为每个请求生成唯一的用户名
req := createReq
req.Username = fmt.Sprintf("benchuser%d", i)
req.Email = fmt.Sprintf("bench%[email protected]", i)
reqBody, _ := json.Marshal(req)
httpReq := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, httpReq)
if w.Code != http.StatusCreated {
b.Errorf("Expected status 201, got %d", w.Code)
}
i++
}
})
}
// 内存分配基准测试
func BenchmarkGetUsersMemory(b *testing.B) {
router := setupTestRouter()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/api/v1/users", nil)
req.Header.Set("Authorization", "Bearer "+generateTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
}
负载测试 #
package loadtest
import (
"context"
"fmt"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
)
type LoadTestResult struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
AverageLatency time.Duration
MaxLatency time.Duration
MinLatency time.Duration
RequestsPerSecond float64
}
func (r *LoadTestResult) SuccessRate() float64 {
if r.TotalRequests == 0 {
return 0
}
return float64(r.SuccessRequests) / float64(r.TotalRequests) * 100
}
func LoadTest(t *testing.T, url string, concurrency int, duration time.Duration) *LoadTestResult {
var (
totalRequests int64
successRequests int64
failedRequests int64
totalLatency int64
maxLatency int64
minLatency int64 = int64(time.Hour) // 初始化为一个大值
)
client := &http.Client{
Timeout: 10 * time.Second,
}
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
var wg sync.WaitGroup
startTime := time.Now()
// 启动并发 goroutine
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
requestStart := time.Now()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
atomic.AddInt64(&failedRequests, 1)
continue
}
req.Header.Set("Authorization", "Bearer "+generateTestToken())
resp, err := client.Do(req)
latency := time.Since(requestStart)
atomic.AddInt64(&totalRequests, 1)
atomic.AddInt64(&totalLatency, int64(latency))
// 更新最大和最小延迟
for {
current := atomic.LoadInt64(&maxLatency)
if int64(latency) <= current {
break
}
if atomic.CompareAndSwapInt64(&maxLatency, current, int64(latency)) {
break
}
}
for {
current := atomic.LoadInt64(&minLatency)
if int64(latency) >= current {
break
}
if atomic.CompareAndSwapInt64(&minLatency, current, int64(latency)) {
break
}
}
if err != nil {
atomic.AddInt64(&failedRequests, 1)
continue
}
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
atomic.AddInt64(&successRequests, 1)
} else {
atomic.AddInt64(&failedRequests, 1)
}
}
}
}()
}
wg.Wait()
actualDuration := time.Since(startTime)
result := &LoadTestResult{
TotalRequests: totalRequests,
SuccessRequests: successRequests,
FailedRequests: failedRequests,
AverageLatency: time.Duration(totalLatency / totalRequests),
MaxLatency: time.Duration(maxLatency),
MinLatency: time.Duration(minLatency),
RequestsPerSecond: float64(totalRequests) / actualDuration.Seconds(),
}
return result
}
func TestAPILoadTest(t *testing.T) {
baseURL := "http://localhost:8080"
tests := []struct {
name string
endpoint string
concurrency int
duration time.Duration
expectRPS float64
}{
{
name: "用户列表端点负载测试",
endpoint: baseURL + "/api/v1/users",
concurrency: 10,
duration: 30 * time.Second,
expectRPS: 100,
},
{
name: "高并发用户列表测试",
endpoint: baseURL + "/api/v1/users",
concurrency: 50,
duration: 60 * time.Second,
expectRPS: 200,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LoadTest(t, tt.endpoint, tt.concurrency, tt.duration)
t.Logf("负载测试结果:")
t.Logf(" 总请求数: %d", result.TotalRequests)
t.Logf(" 成功请求数: %d", result.SuccessRequests)
t.Logf(" 失败请求数: %d", result.FailedRequests)
t.Logf(" 成功率: %.2f%%", result.SuccessRate())
t.Logf(" 平均延迟: %v", result.AverageLatency)
t.Logf(" 最大延迟: %v", result.MaxLatency)
t.Logf(" 最小延迟: %v", result.MinLatency)
t.Logf(" 每秒请求数: %.2f", result.RequestsPerSecond)
// 断言
if result.SuccessRate() < 95.0 {
t.Errorf("成功率过低: %.2f%%, 期望 >= 95%%", result.SuccessRate())
}
if result.RequestsPerSecond < tt.expectRPS {
t.Errorf("RPS 过低: %.2f, 期望 >= %.2f", result.RequestsPerSecond, tt.expectRPS)
}
if result.AverageLatency > 1*time.Second {
t.Errorf("平均延迟过高: %v, 期望 <= 1s", result.AverageLatency)
}
})
}
}
API 调试工具 #
调试中间件 #
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"time"
"github.com/gin-gonic/gin"
)
// DebugMiddleware 调试中间件
func DebugMiddleware() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
})
}
// RequestResponseLogger 请求响应日志中间件
func RequestResponseLogger() gin.HandlerFunc {
return func(c *gin.Context) {
// 记录请求
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
log.Printf("Request: %s %s", c.Request.Method, c.Request.URL.Path)
log.Printf("Headers: %v", c.Request.Header)
if len(requestBody) > 0 {
log.Printf("Body: %s", string(requestBody))
}
// 包装 ResponseWriter 以捕获响应
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
start := time.Now()
c.Next()
latency := time.Since(start)
// 记录响应
log.Printf("Response: %d", blw.Status())
log.Printf("Latency: %v", latency)
log.Printf("Response Body: %s", blw.body.String())
}
}
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// ErrorTrackingMiddleware 错误跟踪中间件
func ErrorTrackingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 检查是否有错误
if len(c.Errors) > 0 {
for _, err := range c.Errors {
log.Printf("Error: %v", err.Error())
log.Printf("Type: %v", err.Type)
log.Printf("Meta: %v", err.Meta)
// 这里可以集成错误监控服务,如 Sentry
// sentry.CaptureException(err.Err)
}
}
}
}
// PerformanceMonitoringMiddleware 性能监控中间件
func PerformanceMonitoringMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
// 记录性能指标
log.Printf("Performance: %s %s - %v",
c.Request.Method,
c.Request.URL.Path,
latency)
// 如果响应时间过长,记录警告
if latency > 1*time.Second {
log.Printf("SLOW REQUEST: %s %s took %v",
c.Request.Method,
c.Request.URL.Path,
latency)
}
// 这里可以发送指标到监控系统
// metrics.RecordLatency(c.Request.Method, c.Request.URL.Path, latency)
}
}
健康检查端点 #
// HealthCheck 健康检查结构
type HealthCheck struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Services map[string]string `json:"services"`
Version string `json:"version"`
Uptime string `json:"uptime"`
}
var startTime = time.Now()
// healthCheckHandler 健康检查处理函数
func healthCheckHandler(c *gin.Context) {
services := make(map[string]string)
// 检查数据库连接
if err := checkDatabase(); err != nil {
services["database"] = "unhealthy: " + err.Error()
} else {
services["database"] = "healthy"
}
// 检查 Redis 连接
if err := checkRedis(); err != nil {
services["redis"] = "unhealthy: " + err.Error()
} else {
services["redis"] = "healthy"
}
// 检查外部 API
if err := checkExternalAPI(); err != nil {
services["external_api"] = "unhealthy: " + err.Error()
} else {
services["external_api"] = "healthy"
}
// 确定整体状态
status := "healthy"
for _, serviceStatus := range services {
if strings.Contains(serviceStatus, "unhealthy") {
status = "unhealthy"
break
}
}
uptime := time.Since(startTime)
healthCheck := HealthCheck{
Status: status,
Timestamp: time.Now(),
Services: services,
Version: "1.0.0",
Uptime: uptime.String(),
}
statusCode := http.StatusOK
if status == "unhealthy" {
statusCode = http.StatusServiceUnavailable
}
c.JSON(statusCode, healthCheck)
}
func checkDatabase() error {
// 实现数据库健康检查
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return db.WithContext(ctx).Raw("SELECT 1").Error
}
func checkRedis() error {
// 实现 Redis 健康检查
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return redisClient.Ping(ctx).Err()
}
func checkExternalAPI() error {
// 实现外部 API 健康检查
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.external.com/health", nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("external API returned status %d", resp.StatusCode)
}
return nil
}
通过这些全面的测试和调试技术,你可以确保 API 的质量、性能和可靠性。测试不仅能帮助发现问题,还能在重构和添加新功能时提供信心保障。