3.5.4 API 测试与调试

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 的质量、性能和可靠性。测试不仅能帮助发现问题,还能在重构和添加新功能时提供信心保障。