第14章:测试与质量保证

本章实战要点

  • 测试金字塔: 单测为基、集成为辅、端到端兜底;表驱动覆盖边界与错误分支。

  • 覆盖策略: 变更相关包≥80%,核心路径优先;并发代码加 -race

  • CI 校验: gofmt/goimports、go vet、golangci-lint、go test -race -cover ./...

参考命令

go test -race -cover ./...
golangci-lint run

交叉引用

  • 第3章并发模型的竞态条件测试;第11章日志用于断言侧写;第13章在流水线上执行测试与报告归档。

覆盖率目标(按包)

包名
目标覆盖率
说明

controller

70%+

路由处理、参数校验、错误分支

service

80%+

业务规则、边界条件、错误路径

router

60%+

路由组与中间件链路

middleware

75%+

认证、限流、CORS、日志拦截

model

70%+

GORM 钩子、约束与事务

dto

60%+

序列化/反序列化与校验

common

70%+

工具与通用方法

setting/config/logger

60%+

配置加载、日志初始化

示例用例清单

  • service/token: 生成/过期/配额耗尽/非法前缀/并发扣额。

  • middleware/rate: 限流窗口与重置、白名单、异常请求计数。

  • controller/auth: 登录成功/失败、锁定、重置流程边界。

  • router: 组级中间件生效顺序、特定路由优先级。

  • model/user: BeforeCreate 加密密码、唯一索引冲突处理。

  • common/hash: 密码校验、随机生成边界(长度/字符集)。

14.1 测试体系概述

14.1.1 测试金字塔

在企业级应用开发中,完善的测试体系是保证代码质量的重要手段。测试金字塔是由Mike Cohn提出的测试策略模型,它指导我们如何分配不同类型测试的比例和投入。

graph TD
    A["端到端测试 (E2E)<br/>• 用户界面测试<br/>• 完整业务流程<br/>• 执行缓慢,成本高<br/>• 数量: 5-10%"] --> B["集成测试 (Integration)<br/>• 模块间交互<br/>• 数据库/API集成<br/>• 中等执行速度<br/>• 数量: 20-30%"]
    B --> C["单元测试 (Unit)<br/>• 函数/方法级别<br/>• 快速反馈<br/>• 执行速度快<br/>• 数量: 60-70%"]
    
    style A fill:#ff9999
    style B fill:#ffcc99
    style C fill:#99ff99

图1 测试金字塔模型

测试金字塔的核心原则

  1. 基础稳固原则:单元测试作为金字塔底层,应该覆盖大部分代码逻辑

  2. 成本效益原则:越往上层,测试成本越高,执行时间越长

  3. 反馈速度原则:底层测试提供快速反馈,顶层测试提供全面验证

  4. 维护成本原则:单元测试易于维护,UI测试维护成本最高

New-API项目中的测试分布

pie title 测试类型分布
    "单元测试" : 65
    "集成测试" : 25
    "端到端测试" : 10

图2 New-API项目测试分布

14.1.2 测试分类

按测试范围分类

  1. 单元测试(Unit Testing)

    • 定义:测试单个函数、方法或类的最小可测试单元

    • 特点:执行速度快(毫秒级),反馈及时,易于调试

    • 覆盖率要求:核心业务逻辑 >80%,工具类 >70%

    • 适用场景:业务逻辑验证、边界条件测试、异常处理

  2. 集成测试(Integration Testing)

    • 定义:测试多个模块或组件之间的交互和协作

    • 类型

      • 组件集成测试:测试内部模块间交互

      • 系统集成测试:测试与外部系统(数据库、缓存、第三方API)的集成

    • 验证重点:数据流转、接口契约、错误传播

  3. 端到端测试(E2E Testing)

    • 定义:从用户角度测试完整的业务流程

    • 特点:模拟真实用户操作,验证系统整体功能

    • 执行环境:接近生产环境的测试环境

    • 关注点:用户体验、业务流程完整性

按测试目的分类

  1. 功能测试(Functional Testing)

    • 验证系统功能是否符合需求规格

    • 包括正向测试和负向测试

  2. 性能测试(Performance Testing)

    • 负载测试:验证系统在预期负载下的性能表现

    • 压力测试:测试系统在极限负载下的稳定性

    • 基准测试:建立性能基线,用于性能回归检测

  3. 安全测试(Security Testing)

    • 验证系统的安全防护能力

    • 包括身份认证、授权、数据加密等

测试策略选择流程

flowchart TD
    A["开始测试规划"] --> B{"确定测试范围"}
    B --> C["单个函数/方法"]
    B --> D["模块间交互"]
    B --> E["完整业务流程"]
    
    C --> F["单元测试"]
    D --> G["集成测试"]
    E --> H["端到端测试"]
    
    F --> I{"是否需要性能验证?"}
    G --> I
    H --> I
    
    I -->|是| J["添加性能测试"]
    I -->|否| K["执行测试"]
    J --> K
    
    K --> L["分析测试结果"]
    L --> M{"测试通过?"}
    M -->|是| N["测试完成"]
    M -->|否| O["修复问题"]
    O --> K

图3 测试策略选择流程

14.2 单元测试实践

14.2.1 测试框架选择

Go语言测试生态概览

Go语言内置了强大的测试支持,同时社区提供了丰富的测试工具。在New-API项目中,我们采用以下测试技术栈:

核心测试框架对比

框架/工具
用途
优势
适用场景

testing

Go内置测试包

零依赖、性能好、官方支持

基础单元测试

testify

断言和测试套件

丰富的断言、Mock支持、易读性强

复杂业务逻辑测试

gomock

Mock生成工具

自动生成Mock、类型安全

接口依赖测试

sqlmock

数据库Mock

无需真实数据库、快速执行

数据层测试

redismock

Redis Mock

模拟缓存操作、隔离测试

缓存层测试

项目测试依赖配置

// go.mod 测试相关依赖
require (
    // 断言和测试套件
    github.com/stretchr/testify v1.8.4
    
    // Mock生成工具
    github.com/golang/mock v1.6.0
    
    // 数据库Mock
    github.com/DATA-DOG/go-sqlmock v1.5.0
    
    // Redis Mock
    github.com/go-redis/redismock/v9 v9.2.0
    
    // HTTP测试工具
    github.com/gavv/httpexpect/v2 v2.15.0
)

测试工具选择决策树

flowchart TD
    A["开始选择测试工具"] --> B{"测试类型?"}
    
    B -->|单元测试| C{"是否需要复杂断言?"}
    B -->|集成测试| D{"是否涉及数据库?"}
    B -->|HTTP API测试| E["httpexpect"]
    
    C -->|是| F["testify + assert"]
    C -->|否| G["标准testing包"]
    
    D -->|是| H["sqlmock"]
    D -->|否| I{"是否涉及Redis?"}
    
    I -->|是| J["redismock"]
    I -->|否| K["testify suite"]
    
    F --> L{"是否需要Mock?"}
    G --> L
    H --> L
    J --> L
    K --> L
    E --> L
    
    L -->|是| M["gomock"]
    L -->|否| N["开始编写测试"]
    M --> N

图4 测试工具选择决策树

14.2.2 用户服务单元测试

单元测试执行流程

单元测试遵循AAA模式(Arrange-Act-Assert),通过Mock对象隔离外部依赖,确保测试的独立性和可重复性。

sequenceDiagram
    participant T as 测试框架
    participant TS as 测试套件
    participant S as 用户服务
    participant MD as Mock数据库
    participant MC as Mock缓存
    participant ML as Mock日志
    
    T->>TS: 1. SetupTest()
    TS->>MD: 创建Mock数据库
    TS->>MC: 创建Mock缓存
    TS->>ML: 创建Mock日志
    TS->>S: 注入Mock依赖
    
    T->>TS: 2. TestCreateUser_Success()
    Note over TS: Arrange阶段
    TS->>MD: 设置期望调用
    TS->>MC: 设置期望调用
    TS->>ML: 设置期望调用
    
    Note over TS: Act阶段
    TS->>S: CreateUser(ctx, user)
    S->>MD: Create(user)
    MD-->>S: 返回成功
    S->>MC: Delete("users:*")
    MC-->>S: 返回成功
    S->>ML: Info("用户创建成功")
    S-->>TS: 返回用户对象
    
    Note over TS: Assert阶段
    TS->>TS: 验证返回结果
    TS->>MD: AssertExpectations()
    TS->>MC: AssertExpectations()
    TS->>ML: AssertExpectations()
    
    T->>TS: 3. TearDownTest()
    TS->>TS: 清理测试环境

图5 单元测试执行时序图

测试代码实现

package service

import (
    "context"
    "errors"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/suite"
    "gorm.io/gorm"
    
    "your-project/model"
)

// 用户服务测试套件
type UserServiceTestSuite struct {
    suite.Suite
    service    *UserService
    mockDB     *MockDB
    mockCache  *MockCache
    mockLogger *MockLogger
}

// 设置测试环境
func (suite *UserServiceTestSuite) SetupTest() {
    suite.mockDB = &MockDB{}
    suite.mockCache = &MockCache{}
    suite.mockLogger = &MockLogger{}
    
    suite.service = &UserService{
        db:     suite.mockDB,
        cache:  suite.mockCache,
        logger: suite.mockLogger,
    }
}

// 测试用户创建 - 成功场景
func (suite *UserServiceTestSuite) TestCreateUser_Success() {
    // Arrange
    user := &model.User{
        Username: "testuser",
        Email:    "[email protected]",
        Password: "hashedpassword",
    }
    
    suite.mockDB.On("Create", mock.AnythingOfType("*model.User")).Return(nil).Run(func(args mock.Arguments) {
        u := args.Get(0).(*model.User)
        u.ID = 1
        u.CreatedAt = time.Now()
    })
    
    suite.mockCache.On("Delete", "users:*").Return(nil)
    suite.mockLogger.On("Info", mock.Anything).Return()
    
    // Act
    result, err := suite.service.CreateUser(context.Background(), user)
    
    // Assert
    assert.NoError(suite.T(), err)
    assert.NotNil(suite.T(), result)
    assert.Equal(suite.T(), uint(1), result.ID)
    assert.Equal(suite.T(), "testuser", result.Username)
    
    suite.mockDB.AssertExpectations(suite.T())
    suite.mockCache.AssertExpectations(suite.T())
}

// 测试用户创建 - 用户名已存在
func (suite *UserServiceTestSuite) TestCreateUser_UsernameExists() {
    // Arrange
    user := &model.User{
        Username: "existinguser",
        Email:    "[email protected]",
        Password: "hashedpassword",
    }
    
    suite.mockDB.On("Create", mock.AnythingOfType("*model.User")).Return(errors.New("UNIQUE constraint failed: users.username"))
    
    // Act
    result, err := suite.service.CreateUser(context.Background(), user)
    
    // Assert
    assert.Error(suite.T(), err)
    assert.Nil(suite.T(), result)
    assert.Contains(suite.T(), err.Error(), "username already exists")
}

// 测试用户查询 - 缓存命中
func (suite *UserServiceTestSuite) TestGetUser_CacheHit() {
    // Arrange
    userID := uint(1)
    cachedUser := &model.User{
        ID:       userID,
        Username: "testuser",
        Email:    "[email protected]",
    }
    
    suite.mockCache.On("Get", "user:1").Return(cachedUser, nil)
    
    // Act
    result, err := suite.service.GetUser(context.Background(), userID)
    
    // Assert
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), cachedUser, result)
    
    // 确保没有调用数据库
    suite.mockDB.AssertNotCalled(suite.T(), "First")
}

// 测试用户查询 - 缓存未命中
func (suite *UserServiceTestSuite) TestGetUser_CacheMiss() {
    // Arrange
    userID := uint(1)
    dbUser := &model.User{
        ID:       userID,
        Username: "testuser",
        Email:    "[email protected]",
    }
    
    suite.mockCache.On("Get", "user:1").Return(nil, errors.New("cache miss"))
    suite.mockDB.On("First", mock.AnythingOfType("*model.User"), userID).Return(nil).Run(func(args mock.Arguments) {
        u := args.Get(0).(*model.User)
        *u = *dbUser
    })
    suite.mockCache.On("Set", "user:1", dbUser, time.Hour).Return(nil)
    
    // Act
    result, err := suite.service.GetUser(context.Background(), userID)
    
    // Assert
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), dbUser, result)
    
    suite.mockDB.AssertExpectations(suite.T())
    suite.mockCache.AssertExpectations(suite.T())
}

// 测试用户更新
func (suite *UserServiceTestSuite) TestUpdateUser_Success() {
    // Arrange
    userID := uint(1)
    updateData := map[string]interface{}{
        "email": "[email protected]",
    }
    
    suite.mockDB.On("Model", mock.AnythingOfType("*model.User")).Return(suite.mockDB)
    suite.mockDB.On("Where", "id = ?", userID).Return(suite.mockDB)
    suite.mockDB.On("Updates", updateData).Return(nil)
    suite.mockCache.On("Delete", "user:1").Return(nil)
    
    // Act
    err := suite.service.UpdateUser(context.Background(), userID, updateData)
    
    // Assert
    assert.NoError(suite.T(), err)
    suite.mockDB.AssertExpectations(suite.T())
    suite.mockCache.AssertExpectations(suite.T())
}

// 运行测试套件
func TestUserServiceSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}

// Mock实现
type MockDB struct {
    mock.Mock
}

func (m *MockDB) Create(value interface{}) error {
    args := m.Called(value)
    return args.Error(0)
}

func (m *MockDB) First(dest interface{}, conds ...interface{}) error {
    args := m.Called(append([]interface{}{dest}, conds...)...)
    return args.Error(0)
}

func (m *MockDB) Model(value interface{}) *MockDB {
    m.Called(value)
    return m
}

func (m *MockDB) Where(query interface{}, args ...interface{}) *MockDB {
    m.Called(append([]interface{}{query}, args...)...)
    return m
}

func (m *MockDB) Updates(values interface{}) error {
    args := m.Called(values)
    return args.Error(0)
}

type MockCache struct {
    mock.Mock
}

func (m *MockCache) Get(key string) (interface{}, error) {
    args := m.Called(key)
    return args.Get(0), args.Error(1)
}

func (m *MockCache) Set(key string, value interface{}, expiration time.Duration) error {
    args := m.Called(key, value, expiration)
    return args.Error(0)
}

func (m *MockCache) Delete(pattern string) error {
    args := m.Called(pattern)
    return args.Error(0)
}

type MockLogger struct {
    mock.Mock
}

func (m *MockLogger) Info(args ...interface{}) {
    m.Called(args...)
}

func (m *MockLogger) Error(args ...interface{}) {
    m.Called(args...)
}

14.2.3 令牌服务单元测试

package service

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    
    "your-project/model"
)

// 测试令牌生成
func TestTokenService_GenerateToken(t *testing.T) {
    tests := []struct {
        name     string
        userID   uint
        tokenType int
        quota    int64
        wantErr  bool
    }{
        {
            name:      "生成访问令牌",
            userID:    1,
            tokenType: model.TokenTypeAccess,
            quota:     1000000,
            wantErr:   false,
        },
        {
            name:      "生成系统令牌",
            userID:    1,
            tokenType: model.TokenTypeSystem,
            quota:     -1, // 无限制
            wantErr:   false,
        },
        {
            name:      "无效用户ID",
            userID:    0,
            tokenType: model.TokenTypeAccess,
            quota:     1000000,
            wantErr:   true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            mockDB := &MockDB{}
            service := &TokenService{db: mockDB}
            
            if !tt.wantErr {
                mockDB.On("Create", mock.AnythingOfType("*model.Token")).Return(nil).Run(func(args mock.Arguments) {
                    token := args.Get(0).(*model.Token)
                    token.ID = 1
                    token.CreatedAt = time.Now()
                })
            }
            
            // Act
            token, err := service.GenerateToken(context.Background(), tt.userID, tt.tokenType, tt.quota)
            
            // Assert
            if tt.wantErr {
                assert.Error(t, err)
                assert.Nil(t, token)
            } else {
                assert.NoError(t, err)
                assert.NotNil(t, token)
                assert.Equal(t, tt.userID, token.UserID)
                assert.Equal(t, tt.tokenType, token.Type)
                assert.Equal(t, tt.quota, token.RemainQuota)
                assert.NotEmpty(t, token.Key)
                assert.Len(t, token.Key, 48) // sk-开头 + 45位随机字符
            }
        })
    }
}

// 测试令牌验证
func TestTokenService_ValidateToken(t *testing.T) {
    tests := []struct {
        name      string
        tokenKey  string
        setupMock func(*MockDB)
        wantToken *model.Token
        wantErr   bool
    }{
        {
            name:     "有效令牌",
            tokenKey: "sk-valid-token-key",
            setupMock: func(mockDB *MockDB) {
                token := &model.Token{
                    ID:          1,
                    Key:         "sk-valid-token-key",
                    UserID:      1,
                    Status:      model.TokenStatusEnabled,
                    RemainQuota: 1000000,
                }
                mockDB.On("Where", "key = ? AND status = ?", "sk-valid-token-key", model.TokenStatusEnabled).Return(mockDB)
                mockDB.On("First", mock.AnythingOfType("*model.Token")).Return(nil).Run(func(args mock.Arguments) {
                    t := args.Get(0).(*model.Token)
                    *t = *token
                })
            },
            wantToken: &model.Token{
                ID:          1,
                Key:         "sk-valid-token-key",
                UserID:      1,
                Status:      model.TokenStatusEnabled,
                RemainQuota: 1000000,
            },
            wantErr: false,
        },
        {
            name:     "令牌不存在",
            tokenKey: "sk-invalid-token-key",
            setupMock: func(mockDB *MockDB) {
                mockDB.On("Where", "key = ? AND status = ?", "sk-invalid-token-key", model.TokenStatusEnabled).Return(mockDB)
                mockDB.On("First", mock.AnythingOfType("*model.Token")).Return(gorm.ErrRecordNotFound)
            },
            wantToken: nil,
            wantErr:   true,
        },
        {
            name:     "配额不足",
            tokenKey: "sk-no-quota-token",
            setupMock: func(mockDB *MockDB) {
                token := &model.Token{
                    ID:          1,
                    Key:         "sk-no-quota-token",
                    UserID:      1,
                    Status:      model.TokenStatusEnabled,
                    RemainQuota: 0,
                }
                mockDB.On("Where", "key = ? AND status = ?", "sk-no-quota-token", model.TokenStatusEnabled).Return(mockDB)
                mockDB.On("First", mock.AnythingOfType("*model.Token")).Return(nil).Run(func(args mock.Arguments) {
                    t := args.Get(0).(*model.Token)
                    *t = *token
                })
            },
            wantToken: nil,
            wantErr:   true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            mockDB := &MockDB{}
            service := &TokenService{db: mockDB}
            tt.setupMock(mockDB)
            
            // Act
            token, err := service.ValidateToken(context.Background(), tt.tokenKey)
            
            // Assert
            if tt.wantErr {
                assert.Error(t, err)
                assert.Nil(t, token)
            } else {
                assert.NoError(t, err)
                assert.Equal(t, tt.wantToken.ID, token.ID)
                assert.Equal(t, tt.wantToken.Key, token.Key)
                assert.Equal(t, tt.wantToken.UserID, token.UserID)
            }
            
            mockDB.AssertExpectations(t)
        })
    }
}

14.2.4 HTTP处理器单元测试

package controller

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    
    "your-project/model"
    "your-project/service"
)

// 设置测试环境
func setupTestRouter() *gin.Engine {
    gin.SetMode(gin.TestMode)
    router := gin.New()
    return router
}

// 测试用户注册接口
func TestUserController_Register(t *testing.T) {
    tests := []struct {
        name           string
        requestBody    interface{}
        setupMock      func(*service.MockUserService)
        expectedStatus int
        expectedBody   map[string]interface{}
    }{
        {
            name: "注册成功",
            requestBody: map[string]interface{}{
                "username": "testuser",
                "email":    "[email protected]",
                "password": "password123",
            },
            setupMock: func(mockService *service.MockUserService) {
                user := &model.User{
                    ID:       1,
                    Username: "testuser",
                    Email:    "[email protected]",
                }
                mockService.On("CreateUser", mock.Anything, mock.AnythingOfType("*model.User")).Return(user, nil)
            },
            expectedStatus: http.StatusCreated,
            expectedBody: map[string]interface{}{
                "success": true,
                "message": "用户注册成功",
                "data": map[string]interface{}{
                    "id":       float64(1),
                    "username": "testuser",
                    "email":    "[email protected]",
                },
            },
        },
        {
            name: "用户名已存在",
            requestBody: map[string]interface{}{
                "username": "existinguser",
                "email":    "[email protected]",
                "password": "password123",
            },
            setupMock: func(mockService *service.MockUserService) {
                mockService.On("CreateUser", mock.Anything, mock.AnythingOfType("*model.User")).Return(nil, errors.New("username already exists"))
            },
            expectedStatus: http.StatusConflict,
            expectedBody: map[string]interface{}{
                "success": false,
                "message": "用户名已存在",
            },
        },
        {
            name: "请求参数无效",
            requestBody: map[string]interface{}{
                "username": "",
                "email":    "invalid-email",
                "password": "123", // 密码太短
            },
            setupMock:      func(mockService *service.MockUserService) {},
            expectedStatus: http.StatusBadRequest,
            expectedBody: map[string]interface{}{
                "success": false,
                "message": "请求参数无效",
            },
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            mockService := &service.MockUserService{}
            tt.setupMock(mockService)
            
            controller := &UserController{
                userService: mockService,
            }
            
            router := setupTestRouter()
            router.POST("/register", controller.Register)
            
            requestBody, _ := json.Marshal(tt.requestBody)
            req := httptest.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(requestBody))
            req.Header.Set("Content-Type", "application/json")
            
            w := httptest.NewRecorder()
            
            // Act
            router.ServeHTTP(w, req)
            
            // Assert
            assert.Equal(t, tt.expectedStatus, w.Code)
            
            var response map[string]interface{}
            err := json.Unmarshal(w.Body.Bytes(), &response)
            assert.NoError(t, err)
            
            assert.Equal(t, tt.expectedBody["success"], response["success"])
            assert.Equal(t, tt.expectedBody["message"], response["message"])
            
            if tt.expectedBody["data"] != nil {
                assert.Equal(t, tt.expectedBody["data"], response["data"])
            }
            
            mockService.AssertExpectations(t)
        })
    }
}

// 测试令牌创建接口
func TestTokenController_CreateToken(t *testing.T) {
    tests := []struct {
        name           string
        userID         uint
        requestBody    interface{}
        setupMock      func(*service.MockTokenService)
        expectedStatus int
    }{
        {
            name:   "创建令牌成功",
            userID: 1,
            requestBody: map[string]interface{}{
                "name":  "测试令牌",
                "quota": 1000000,
            },
            setupMock: func(mockService *service.MockTokenService) {
                token := &model.Token{
                    ID:          1,
                    Key:         "sk-test-token-key",
                    Name:        "测试令牌",
                    UserID:      1,
                    RemainQuota: 1000000,
                }
                mockService.On("GenerateToken", mock.Anything, uint(1), model.TokenTypeAccess, int64(1000000)).Return(token, nil)
            },
            expectedStatus: http.StatusCreated,
        },
        {
            name:   "配额无效",
            userID: 1,
            requestBody: map[string]interface{}{
                "name":  "测试令牌",
                "quota": -100, // 负数配额
            },
            setupMock:      func(mockService *service.MockTokenService) {},
            expectedStatus: http.StatusBadRequest,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            mockService := &service.MockTokenService{}
            tt.setupMock(mockService)
            
            controller := &TokenController{
                tokenService: mockService,
            }
            
            router := setupTestRouter()
            router.POST("/tokens", func(c *gin.Context) {
                c.Set("user_id", tt.userID)
                controller.CreateToken(c)
            })
            
            requestBody, _ := json.Marshal(tt.requestBody)
            req := httptest.NewRequest(http.MethodPost, "/tokens", bytes.NewBuffer(requestBody))
            req.Header.Set("Content-Type", "application/json")
            
            w := httptest.NewRecorder()
            
            // Act
            router.ServeHTTP(w, req)
            
            // Assert
            assert.Equal(t, tt.expectedStatus, w.Code)
            mockService.AssertExpectations(t)
        })
    }
}

14.3 集成测试

集成测试验证多个组件或模块之间的交互是否正确。与单元测试不同,集成测试使用真实的外部依赖(如数据库、缓存、消息队列等),确保系统各部分能够正确协作。

集成测试架构

graph TB
    subgraph "测试环境"
        TC["测试用例"]
        TS["测试套件"]
    end
    
    subgraph "应用层"
        API["HTTP API"]
        SVC["业务服务"]
        REPO["数据仓库"]
    end
    
    subgraph "基础设施层"
        DB[("测试数据库<br/>SQLite/PostgreSQL")]
        CACHE[("测试缓存<br/>Redis")]
        MQ[("消息队列<br/>RabbitMQ")]
    end
    
    TC --> TS
    TS --> API
    API --> SVC
    SVC --> REPO
    REPO --> DB
    SVC --> CACHE
    SVC --> MQ
    
    style TC fill:#e1f5fe
    style API fill:#f3e5f5
    style SVC fill:#e8f5e8
    style DB fill:#fff3e0
    style CACHE fill:#fce4ec
    style MQ fill:#f1f8e9

图6 集成测试架构图

集成测试分类

14.3.1 数据库集成测试

数据库集成测试验证应用与数据库的交互,包括CRUD操作、事务处理、约束检查等。

测试环境准备流程

flowchart TD
    A["开始测试"] --> B["创建测试数据库"]
    B --> C["执行数据库迁移"]
    C --> D["准备测试数据"]
    D --> E["执行测试用例"]
    E --> F{"测试通过?"}
    F -->|是| G["清理测试数据"]
    F -->|否| H["记录失败信息"]
    H --> G
    G --> I["销毁测试数据库"]
    I --> J["测试结束"]

图7 数据库集成测试流程

package integration

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    
    "your-project/model"
    "your-project/service"
)

// 数据库集成测试套件
type DatabaseIntegrationTestSuite struct {
    suite.Suite
    db          *gorm.DB
    userService *service.UserService
}

// 设置测试数据库
func (suite *DatabaseIntegrationTestSuite) SetupSuite() {
    // 使用内存SQLite数据库
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    suite.Require().NoError(err)
    
    // 自动迁移
    err = db.AutoMigrate(&model.User{}, &model.Token{}, &model.Channel{})
    suite.Require().NoError(err)
    
    suite.db = db
    suite.userService = service.NewUserService(db, nil, nil)
}

// 清理测试数据
func (suite *DatabaseIntegrationTestSuite) SetupTest() {
    // 清空所有表
    suite.db.Exec("DELETE FROM users")
    suite.db.Exec("DELETE FROM tokens")
    suite.db.Exec("DELETE FROM channels")
}

// 测试用户CRUD操作
func (suite *DatabaseIntegrationTestSuite) TestUserCRUD() {
    ctx := context.Background()
    
    // 创建用户
    user := &model.User{
        Username: "testuser",
        Email:    "[email protected]",
        Password: "hashedpassword",
    }
    
    createdUser, err := suite.userService.CreateUser(ctx, user)
    assert.NoError(suite.T(), err)
    assert.NotZero(suite.T(), createdUser.ID)
    assert.Equal(suite.T(), "testuser", createdUser.Username)
    
    // 查询用户
    foundUser, err := suite.userService.GetUser(ctx, createdUser.ID)
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), createdUser.ID, foundUser.ID)
    assert.Equal(suite.T(), "testuser", foundUser.Username)
    
    // 更新用户
    updateData := map[string]interface{}{
        "email": "[email protected]",
    }
    err = suite.userService.UpdateUser(ctx, createdUser.ID, updateData)
    assert.NoError(suite.T(), err)
    
    // 验证更新
    updatedUser, err := suite.userService.GetUser(ctx, createdUser.ID)
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), "[email protected]", updatedUser.Email)
    
    // 删除用户
    err = suite.userService.DeleteUser(ctx, createdUser.ID)
    assert.NoError(suite.T(), err)
    
    // 验证删除
    _, err = suite.userService.GetUser(ctx, createdUser.ID)
    assert.Error(suite.T(), err)
}

// 测试用户和令牌关联
func (suite *DatabaseIntegrationTestSuite) TestUserTokenRelation() {
    ctx := context.Background()
    
    // 创建用户
    user := &model.User{
        Username: "testuser",
        Email:    "[email protected]",
        Password: "hashedpassword",
    }
    
    createdUser, err := suite.userService.CreateUser(ctx, user)
    assert.NoError(suite.T(), err)
    
    // 创建令牌服务
    tokenService := service.NewTokenService(suite.db, nil)
    
    // 为用户创建令牌
    token, err := tokenService.GenerateToken(ctx, createdUser.ID, model.TokenTypeAccess, 1000000)
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), createdUser.ID, token.UserID)
    
    // 查询用户的所有令牌
    tokens, err := tokenService.GetUserTokens(ctx, createdUser.ID)
    assert.NoError(suite.T(), err)
    assert.Len(suite.T(), tokens, 1)
    assert.Equal(suite.T(), token.ID, tokens[0].ID)
}

// 运行集成测试套件
func TestDatabaseIntegrationSuite(t *testing.T) {
    suite.Run(t, new(DatabaseIntegrationTestSuite))
}

14.3.2 Redis集成测试

package integration

import (
    "context"
    "testing"
    "time"
    
    "github.com/go-redis/redis/v8"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
    
    "your-project/service"
)

// Redis集成测试套件
type RedisIntegrationTestSuite struct {
    suite.Suite
    client      *redis.Client
    cacheService *service.CacheService
}

// 设置Redis客户端
func (suite *RedisIntegrationTestSuite) SetupSuite() {
    // 连接到测试Redis实例
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       15, // 使用测试数据库
    })
    
    // 测试连接
    ctx := context.Background()
    _, err := client.Ping(ctx).Result()
    if err != nil {
        suite.T().Skip("Redis not available, skipping integration tests")
        return
    }
    
    suite.client = client
    suite.cacheService = service.NewCacheService(client)
}

// 清理测试数据
func (suite *RedisIntegrationTestSuite) SetupTest() {
    if suite.client != nil {
        suite.client.FlushDB(context.Background())
    }
}

// 清理资源
func (suite *RedisIntegrationTestSuite) TearDownSuite() {
    if suite.client != nil {
        suite.client.Close()
    }
}

// 测试基本缓存操作
func (suite *RedisIntegrationTestSuite) TestBasicCacheOperations() {
    if suite.client == nil {
        suite.T().Skip("Redis not available")
        return
    }
    
    ctx := context.Background()
    
    // 测试Set和Get
    key := "test:key"
    value := "test value"
    
    err := suite.cacheService.Set(ctx, key, value, time.Minute)
    assert.NoError(suite.T(), err)
    
    result, err := suite.cacheService.Get(ctx, key)
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), value, result)
    
    // 测试过期时间
    ttl, err := suite.client.TTL(ctx, key).Result()
    assert.NoError(suite.T(), err)
    assert.True(suite.T(), ttl > 0 && ttl <= time.Minute)
    
    // 测试删除
    err = suite.cacheService.Delete(ctx, key)
    assert.NoError(suite.T(), err)
    
    _, err = suite.cacheService.Get(ctx, key)
    assert.Error(suite.T(), err)
}

// 测试缓存模式匹配删除
func (suite *RedisIntegrationTestSuite) TestCachePatternDelete() {
    if suite.client == nil {
        suite.T().Skip("Redis not available")
        return
    }
    
    ctx := context.Background()
    
    // 设置多个相关键
    keys := []string{"user:1", "user:2", "user:3", "token:1"}
    for _, key := range keys {
        err := suite.cacheService.Set(ctx, key, "value", time.Minute)
        assert.NoError(suite.T(), err)
    }
    
    // 删除用户相关的缓存
    err := suite.cacheService.DeletePattern(ctx, "user:*")
    assert.NoError(suite.T(), err)
    
    // 验证用户缓存已删除
    for i := 1; i <= 3; i++ {
        _, err := suite.cacheService.Get(ctx, fmt.Sprintf("user:%d", i))
        assert.Error(suite.T(), err)
    }
    
    // 验证令牌缓存仍存在
    _, err = suite.cacheService.Get(ctx, "token:1")
    assert.NoError(suite.T(), err)
}

// 运行Redis集成测试套件
func TestRedisIntegrationSuite(t *testing.T) {
    suite.Run(t, new(RedisIntegrationTestSuite))
}

14.3.3 HTTP API集成测试

package integration

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
    
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    
    "your-project/controller"
    "your-project/middleware"
    "your-project/model"
    "your-project/router"
    "your-project/service"
)

// API集成测试套件
type APIIntegrationTestSuite struct {
    suite.Suite
    app    *gin.Engine
    db     *gorm.DB
    server *httptest.Server
}

// 设置测试环境
func (suite *APIIntegrationTestSuite) SetupSuite() {
    gin.SetMode(gin.TestMode)
    
    // 设置测试数据库
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    suite.Require().NoError(err)
    
    err = db.AutoMigrate(&model.User{}, &model.Token{}, &model.Channel{})
    suite.Require().NoError(err)
    
    suite.db = db
    
    // 创建服务
    userService := service.NewUserService(db, nil, nil)
    tokenService := service.NewTokenService(db, nil)
    
    // 创建控制器
    userController := controller.NewUserController(userService)
    tokenController := controller.NewTokenController(tokenService)
    
    // 设置路由
    app := gin.New()
    app.Use(gin.Recovery())
    
    // 公开路由
    public := app.Group("/api/v1")
    public.POST("/register", userController.Register)
    public.POST("/login", userController.Login)
    
    // 需要认证的路由
    auth := app.Group("/api/v1")
    auth.Use(middleware.AuthMiddleware(tokenService))
    auth.GET("/profile", userController.GetProfile)
    auth.PUT("/profile", userController.UpdateProfile)
    auth.GET("/tokens", tokenController.ListTokens)
    auth.POST("/tokens", tokenController.CreateToken)
    auth.DELETE("/tokens/:id", tokenController.DeleteToken)
    
    suite.app = app
    suite.server = httptest.NewServer(app)
}

// 清理测试数据
func (suite *APIIntegrationTestSuite) SetupTest() {
    suite.db.Exec("DELETE FROM users")
    suite.db.Exec("DELETE FROM tokens")
}

// 清理资源
func (suite *APIIntegrationTestSuite) TearDownSuite() {
    if suite.server != nil {
        suite.server.Close()
    }
}

// 测试用户注册和登录流程
func (suite *APIIntegrationTestSuite) TestUserRegistrationAndLogin() {
    // 注册用户
    registerData := map[string]interface{}{
        "username": "testuser",
        "email":    "[email protected]",
        "password": "password123",
    }
    
    registerResp := suite.makeRequest("POST", "/api/v1/register", registerData, "")
    assert.Equal(suite.T(), http.StatusCreated, registerResp.Code)
    
    var registerResult map[string]interface{}
    err := json.Unmarshal(registerResp.Body.Bytes(), &registerResult)
    assert.NoError(suite.T(), err)
    assert.True(suite.T(), registerResult["success"].(bool))
    
    // 登录用户
    loginData := map[string]interface{}{
        "username": "testuser",
        "password": "password123",
    }
    
    loginResp := suite.makeRequest("POST", "/api/v1/login", loginData, "")
    assert.Equal(suite.T(), http.StatusOK, loginResp.Code)
    
    var loginResult map[string]interface{}
    err = json.Unmarshal(loginResp.Body.Bytes(), &loginResult)
    assert.NoError(suite.T(), err)
    assert.True(suite.T(), loginResult["success"].(bool))
    
    data := loginResult["data"].(map[string]interface{})
    token := data["token"].(string)
    assert.NotEmpty(suite.T(), token)
}

// 测试令牌管理流程
func (suite *APIIntegrationTestSuite) TestTokenManagement() {
    // 先注册和登录用户
    token := suite.createUserAndGetToken()
    
    // 创建令牌
    createTokenData := map[string]interface{}{
        "name":  "测试令牌",
        "quota": 1000000,
    }
    
    createResp := suite.makeRequest("POST", "/api/v1/tokens", createTokenData, token)
    assert.Equal(suite.T(), http.StatusCreated, createResp.Code)
    
    var createResult map[string]interface{}
    err := json.Unmarshal(createResp.Body.Bytes(), &createResult)
    assert.NoError(suite.T(), err)
    assert.True(suite.T(), createResult["success"].(bool))
    
    data := createResult["data"].(map[string]interface{})
    tokenID := data["id"].(float64)
    
    // 列出令牌
    listResp := suite.makeRequest("GET", "/api/v1/tokens", nil, token)
    assert.Equal(suite.T(), http.StatusOK, listResp.Code)
    
    var listResult map[string]interface{}
    err = json.Unmarshal(listResp.Body.Bytes(), &listResult)
    assert.NoError(suite.T(), err)
    assert.True(suite.T(), listResult["success"].(bool))
    
    tokens := listResult["data"].([]interface{})
    assert.Len(suite.T(), tokens, 1)
    
    // 删除令牌
    deleteResp := suite.makeRequest("DELETE", fmt.Sprintf("/api/v1/tokens/%d", int(tokenID)), nil, token)
    assert.Equal(suite.T(), http.StatusOK, deleteResp.Code)
    
    // 验证令牌已删除
    listResp2 := suite.makeRequest("GET", "/api/v1/tokens", nil, token)
    assert.Equal(suite.T(), http.StatusOK, listResp2.Code)
    
    var listResult2 map[string]interface{}
    err = json.Unmarshal(listResp2.Body.Bytes(), &listResult2)
    assert.NoError(suite.T(), err)
    
    tokens2 := listResult2["data"].([]interface{})
    assert.Len(suite.T(), tokens2, 0)
}

// 测试认证中间件
func (suite *APIIntegrationTestSuite) TestAuthMiddleware() {
    // 未提供令牌
    resp := suite.makeRequest("GET", "/api/v1/profile", nil, "")
    assert.Equal(suite.T(), http.StatusUnauthorized, resp.Code)
    
    // 无效令牌
    resp = suite.makeRequest("GET", "/api/v1/profile", nil, "invalid-token")
    assert.Equal(suite.T(), http.StatusUnauthorized, resp.Code)
    
    // 有效令牌
    token := suite.createUserAndGetToken()
    resp = suite.makeRequest("GET", "/api/v1/profile", nil, token)
    assert.Equal(suite.T(), http.StatusOK, resp.Code)
}

// 辅助方法:创建用户并获取令牌
func (suite *APIIntegrationTestSuite) createUserAndGetToken() string {
    // 注册用户
    registerData := map[string]interface{}{
        "username": "testuser",
        "email":    "[email protected]",
        "password": "password123",
    }
    
    suite.makeRequest("POST", "/api/v1/register", registerData, "")
    
    // 登录获取令牌
    loginData := map[string]interface{}{
        "username": "testuser",
        "password": "password123",
    }
    
    loginResp := suite.makeRequest("POST", "/api/v1/login", loginData, "")
    
    var loginResult map[string]interface{}
    json.Unmarshal(loginResp.Body.Bytes(), &loginResult)
    
    data := loginResult["data"].(map[string]interface{})
    return data["token"].(string)
}

// 辅助方法:发送HTTP请求
func (suite *APIIntegrationTestSuite) makeRequest(method, path string, body interface{}, token string) *httptest.ResponseRecorder {
    var reqBody *bytes.Buffer
    
    if body != nil {
        jsonBody, _ := json.Marshal(body)
        reqBody = bytes.NewBuffer(jsonBody)
    } else {
        reqBody = bytes.NewBuffer(nil)
    }
    
    req := httptest.NewRequest(method, path, reqBody)
    req.Header.Set("Content-Type", "application/json")
    
    if token != "" {
        req.Header.Set("Authorization", "Bearer "+token)
    }
    
    w := httptest.NewRecorder()
    suite.app.ServeHTTP(w, req)
    
    return w
}

// 运行API集成测试套件
func TestAPIIntegrationSuite(t *testing.T) {
    suite.Run(t, new(APIIntegrationTestSuite))
}

14.4 端到端测试

**端到端测试(End-to-End Testing)**从用户角度验证整个应用的功能,模拟真实用户操作,测试完整的业务流程。E2E测试确保前端、后端、数据库等所有组件协同工作正常。

E2E测试架构

graph TB
    subgraph "测试执行环境"
        TC["测试用例"]
        PW["Playwright"]
        BR["浏览器<br/>(Chrome/Firefox/Safari)"]
    end
    
    subgraph "前端应用"
        UI["用户界面"]
        JS["JavaScript"]
        API_CLIENT["API客户端"]
    end
    
    subgraph "后端服务"
        HTTP["HTTP服务器"]
        AUTH["认证服务"]
        BIZ["业务逻辑"]
    end
    
    subgraph "数据存储"
        DB[("数据库")]
        CACHE[("缓存")]
    end
    
    TC --> PW
    PW --> BR
    BR --> UI
    UI --> JS
    JS --> API_CLIENT
    API_CLIENT --> HTTP
    HTTP --> AUTH
    HTTP --> BIZ
    BIZ --> DB
    BIZ --> CACHE
    
    style TC fill:#e3f2fd
    style PW fill:#f3e5f5
    style BR fill:#e8f5e8
    style UI fill:#fff3e0
    style HTTP fill:#fce4ec
    style DB fill:#f1f8e9

图8 端到端测试架构图

用户操作流程测试

sequenceDiagram
    participant T as 测试脚本
    participant B as 浏览器
    participant F as 前端应用
    participant A as 后端API
    participant D as 数据库
    
    T->>B: 启动浏览器
    T->>B: 导航到登录页面
    B->>F: 加载页面
    F-->>B: 显示登录表单
    
    T->>B: 输入用户名密码
    T->>B: 点击登录按钮
    B->>F: 提交表单
    F->>A: POST /api/v1/login
    A->>D: 验证用户凭据
    D-->>A: 返回用户信息
    A-->>F: 返回JWT令牌
    F-->>B: 跳转到仪表板
    
    T->>B: 验证页面元素
    T->>B: 执行业务操作
    B->>F: 用户交互
    F->>A: API调用
    A->>D: 数据操作
    D-->>A: 返回结果
    A-->>F: 响应数据
    F-->>B: 更新界面
    
    T->>T: 断言验证结果

图9 E2E测试用户操作时序图

14.4.1 使用Playwright进行E2E测试

Playwright是微软开发的现代化端到端测试框架,支持多浏览器、多平台,提供强大的自动化测试能力。

package e2e

import (
    "context"
    "testing"
    "time"
    
    "github.com/playwright-community/playwright-go"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
)

// E2E测试套件
type E2ETestSuite struct {
    suite.Suite
    pw      *playwright.Playwright
    browser playwright.Browser
    baseURL string
}

// 设置Playwright环境
func (suite *E2ETestSuite) SetupSuite() {
    pw, err := playwright.Run()
    suite.Require().NoError(err)
    
    browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
        Headless: playwright.Bool(true),
    })
    suite.Require().NoError(err)
    
    suite.pw = pw
    suite.browser = browser
    suite.baseURL = "http://localhost:3000" // 前端应用地址
}

// 清理资源
func (suite *E2ETestSuite) TearDownSuite() {
    if suite.browser != nil {
        suite.browser.Close()
    }
    if suite.pw != nil {
        suite.pw.Stop()
    }
}

// 测试用户注册流程
func (suite *E2ETestSuite) TestUserRegistration() {
    page, err := suite.browser.NewPage()
    suite.Require().NoError(err)
    defer page.Close()
    
    // 访问注册页面
    _, err = page.Goto(suite.baseURL + "/register")
    assert.NoError(suite.T(), err)
    
    // 填写注册表单
    err = page.Fill("input[name='username']", "testuser")
    assert.NoError(suite.T(), err)
    
    err = page.Fill("input[name='email']", "[email protected]")
    assert.NoError(suite.T(), err)
    
    err = page.Fill("input[name='password']", "password123")
    assert.NoError(suite.T(), err)
    
    err = page.Fill("input[name='confirmPassword']", "password123")
    assert.NoError(suite.T(), err)
    
    // 提交表单
    err = page.Click("button[type='submit']")
    assert.NoError(suite.T(), err)
    
    // 等待跳转到登录页面
    err = page.WaitForURL(suite.baseURL + "/login")
    assert.NoError(suite.T(), err)
    
    // 验证成功消息
    successMessage, err := page.Locator(".success-message").TextContent()
    assert.NoError(suite.T(), err)
    assert.Contains(suite.T(), successMessage, "注册成功")
}

// 测试用户登录流程
func (suite *E2ETestSuite) TestUserLogin() {
    page, err := suite.browser.NewPage()
    suite.Require().NoError(err)
    defer page.Close()
    
    // 先注册用户(可以调用API或使用已存在的用户)
    suite.createTestUser()
    
    // 访问登录页面
    _, err = page.Goto(suite.baseURL + "/login")
    assert.NoError(suite.T(), err)
    
    // 填写登录表单
    err = page.Fill("input[name='username']", "testuser")
    assert.NoError(suite.T(), err)
    
    err = page.Fill("input[name='password']", "password123")
    assert.NoError(suite.T(), err)
    
    // 提交表单
    err = page.Click("button[type='submit']")
    assert.NoError(suite.T(), err)
    
    // 等待跳转到仪表板
    err = page.WaitForURL(suite.baseURL + "/dashboard")
    assert.NoError(suite.T(), err)
    
    // 验证用户信息显示
    username, err := page.Locator(".user-info .username").TextContent()
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), "testuser", username)
}

// 测试令牌管理流程
func (suite *E2ETestSuite) TestTokenManagement() {
    page, err := suite.browser.NewPage()
    suite.Require().NoError(err)
    defer page.Close()
    
    // 登录用户
    suite.loginUser(page)
    
    // 访问令牌管理页面
    err = page.Click("a[href='/tokens']")
    assert.NoError(suite.T(), err)
    
    err = page.WaitForURL(suite.baseURL + "/tokens")
    assert.NoError(suite.T(), err)
    
    // 创建新令牌
    err = page.Click("button.create-token")
    assert.NoError(suite.T(), err)
    
    // 填写令牌信息
    err = page.Fill("input[name='name']", "测试令牌")
    assert.NoError(suite.T(), err)
    
    err = page.Fill("input[name='quota']", "1000000")
    assert.NoError(suite.T(), err)
    
    // 提交创建
    err = page.Click("button.submit-create")
    assert.NoError(suite.T(), err)
    
    // 等待令牌列表更新
    err = page.WaitForSelector(".token-item")
    assert.NoError(suite.T(), err)
    
    // 验证令牌已创建
    tokenName, err := page.Locator(".token-item .token-name").First().TextContent()
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), "测试令牌", tokenName)
    
    // 测试令牌删除
    err = page.Click(".token-item .delete-button")
    assert.NoError(suite.T(), err)
    
    // 确认删除
    err = page.Click(".confirm-delete")
    assert.NoError(suite.T(), err)
    
    // 验证令牌已删除
    tokenCount, err := page.Locator(".token-item").Count()
    assert.NoError(suite.T(), err)
    assert.Equal(suite.T(), 0, tokenCount)
}

// 辅助方法:创建测试用户
func (suite *E2ETestSuite) createTestUser() {
    // 这里可以直接调用API创建用户,或者通过UI创建
    // 为了简化,假设用户已存在
}

// 辅助方法:用户登录
func (suite *E2ETestSuite) loginUser(page playwright.Page) {
    page.Goto(suite.baseURL + "/login")
    page.Fill("input[name='username']", "testuser")
    page.Fill("input[name='password']", "password123")
    page.Click("button[type='submit']")
    page.WaitForURL(suite.baseURL + "/dashboard")
}

// 运行E2E测试套件
func TestE2ETestSuite(t *testing.T) {
    suite.Run(t, new(E2ETestSuite))
}

14.5 性能测试

性能测试评估系统在不同负载条件下的性能表现,包括响应时间、吞吐量、资源利用率等关键指标。性能测试帮助识别性能瓶颈,确保系统能够满足预期的性能要求。

性能测试分类

graph TD
    A["性能测试"] --> B["基准测试<br/>Benchmark"]
    A --> C["负载测试<br/>Load Testing"]
    A --> D["压力测试<br/>Stress Testing"]
    A --> E["容量测试<br/>Volume Testing"]
    A --> F["稳定性测试<br/>Endurance Testing"]
    
    B --> B1["单个函数性能"]
    B --> B2["算法效率对比"]
    B --> B3["内存分配分析"]
    
    C --> C1["预期负载下性能"]
    C --> C2["响应时间验证"]
    C --> C3["吞吐量测试"]
    
    D --> D1["极限负载测试"]
    D --> D2["故障恢复能力"]
    D --> D3["资源耗尽场景"]
    
    E --> E1["大数据量处理"]
    E --> E2["存储容量测试"]
    
    F --> F1["长时间运行"]
    F --> F2["内存泄漏检测"]
    
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#fce4ec
    style F fill:#f1f8e9

图10 性能测试分类图

性能测试架构

graph TB
    subgraph "测试工具层"
        BT["基准测试<br/>go test -bench"]
        LT["负载测试<br/>Artillery/K6"]
        PT["性能分析<br/>pprof"]
    end
    
    subgraph "应用层"
        API["HTTP API"]
        SVC["业务服务"]
        CACHE["缓存层"]
    end
    
    subgraph "数据层"
        DB[("数据库")]
        REDIS[("Redis")]
    end
    
    subgraph "监控层"
        METRICS["指标收集"]
        GRAFANA["可视化"]
        ALERT["告警"]
    end
    
    BT --> API
    LT --> API
    PT --> SVC
    
    API --> SVC
    SVC --> CACHE
    SVC --> DB
    CACHE --> REDIS
    
    API --> METRICS
    SVC --> METRICS
    DB --> METRICS
    REDIS --> METRICS
    
    METRICS --> GRAFANA
    METRICS --> ALERT
    
    style BT fill:#e3f2fd
    style API fill:#f3e5f5
    style SVC fill:#e8f5e8
    style DB fill:#fff3e0
    style METRICS fill:#fce4ec

图11 性能测试架构图

14.5.1 基准测试

**基准测试(Benchmark)**测量代码的执行性能,包括执行时间、内存分配、CPU使用率等。Go语言内置了强大的基准测试支持。

基准测试执行流程

flowchart TD
    A["开始基准测试"] --> B["初始化测试环境"]
    B --> C["预热阶段"]
    C --> D["重置计时器"]
    D --> E["执行测试循环"]
    E --> F{"达到稳定状态?"}
    F -->|否| G["调整迭代次数"]
    G --> E
    F -->|是| H["收集性能指标"]
    H --> I["生成测试报告"]
    I --> J["性能分析"]
    J --> K["结束测试"]

图12 基准测试执行流程

package benchmark

import (
    "context"
    "testing"
    "time"
    
    "your-project/service"
    "your-project/model"
)

// 用户服务基准测试
func BenchmarkUserService_CreateUser(b *testing.B) {
    // 设置测试环境
    db := setupTestDB()
    userService := service.NewUserService(db, nil, nil)
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        user := &model.User{
            Username: fmt.Sprintf("user%d", i),
            Email:    fmt.Sprintf("user%[email protected]", i),
            Password: "hashedpassword",
        }
        
        _, err := userService.CreateUser(context.Background(), user)
        if err != nil {
            b.Fatal(err)
        }
    }
}

// 令牌验证基准测试
func BenchmarkTokenService_ValidateToken(b *testing.B) {
    db := setupTestDB()
    tokenService := service.NewTokenService(db, nil)
    
    // 预创建令牌
    token, _ := tokenService.GenerateToken(context.Background(), 1, model.TokenTypeAccess, 1000000)
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        _, err := tokenService.ValidateToken(context.Background(), token.Key)
        if err != nil {
            b.Fatal(err)
        }
    }
}

// 缓存性能基准测试
func BenchmarkCacheService_SetGet(b *testing.B) {
    cacheService := setupTestCache()
    ctx := context.Background()
    
    b.Run("Set", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            key := fmt.Sprintf("key%d", i)
            value := fmt.Sprintf("value%d", i)
            
            err := cacheService.Set(ctx, key, value, time.Hour)
            if err != nil {
                b.Fatal(err)
            }
        }
    })
    
    // 预设置一些键值对
    for i := 0; i < 1000; i++ {
        key := fmt.Sprintf("key%d", i)
        value := fmt.Sprintf("value%d", i)
        cacheService.Set(ctx, key, value, time.Hour)
    }
    
    b.Run("Get", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            key := fmt.Sprintf("key%d", i%1000)
            
            _, err := cacheService.Get(ctx, key)
            if err != nil {
                b.Fatal(err)
            }
        }
    })
}

// 并发性能测试
func BenchmarkConcurrentTokenValidation(b *testing.B) {
    db := setupTestDB()
    tokenService := service.NewTokenService(db, nil)
    
    // 预创建令牌
    tokens := make([]*model.Token, 100)
    for i := 0; i < 100; i++ {
        token, _ := tokenService.GenerateToken(context.Background(), uint(i+1), model.TokenTypeAccess, 1000000)
        tokens[i] = token
    }
    
    b.ResetTimer()
    
    b.RunParallel(func(pb *testing.PB) {
        i := 0
        for pb.Next() {
            token := tokens[i%len(tokens)]
            _, err := tokenService.ValidateToken(context.Background(), token.Key)
            if err != nil {
                b.Fatal(err)
            }
            i++
        }
    })
}

14.5.2 负载测试

package loadtest

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "testing"
    "time"
)

// 负载测试配置
type LoadTestConfig struct {
    BaseURL     string
    Concurrency int
    Duration    time.Duration
    RampUp      time.Duration
}

// 负载测试结果
type LoadTestResult struct {
    TotalRequests   int
    SuccessRequests int
    FailedRequests  int
    AverageLatency  time.Duration
    MaxLatency      time.Duration
    MinLatency      time.Duration
    RequestsPerSec  float64
}

// 负载测试执行器
type LoadTester struct {
    config LoadTestConfig
    client *http.Client
}

// 创建负载测试器
func NewLoadTester(config LoadTestConfig) *LoadTester {
    return &LoadTester{
        config: config,
        client: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

// 执行登录接口负载测试
func (lt *LoadTester) TestLoginEndpoint() *LoadTestResult {
    var (
        totalRequests   int
        successRequests int
        failedRequests  int
        latencies       []time.Duration
        mu              sync.Mutex
        wg              sync.WaitGroup
    )
    
    startTime := time.Now()
    endTime := startTime.Add(lt.config.Duration)
    
    // 创建工作协程
    for i := 0; i < lt.config.Concurrency; i++ {
        wg.Add(1)
        
        go func(workerID int) {
            defer wg.Done()
            
            // 渐进式启动
            if lt.config.RampUp > 0 {
                delay := time.Duration(workerID) * lt.config.RampUp / time.Duration(lt.config.Concurrency)
                time.Sleep(delay)
            }
            
            for time.Now().Before(endTime) {
                start := time.Now()
                
                // 发送登录请求
                success := lt.sendLoginRequest(workerID)
                
                latency := time.Since(start)
                
                mu.Lock()
                totalRequests++
                latencies = append(latencies, latency)
                if success {
                    successRequests++
                } else {
                    failedRequests++
                }
                mu.Unlock()
                
                // 控制请求频率
                time.Sleep(10 * time.Millisecond)
            }
        }(i)
    }
    
    wg.Wait()
    
    // 计算统计结果
    duration := time.Since(startTime)
    
    var totalLatency time.Duration
    maxLatency := time.Duration(0)
    minLatency := time.Duration(1<<63 - 1)
    
    for _, latency := range latencies {
        totalLatency += latency
        if latency > maxLatency {
            maxLatency = latency
        }
        if latency < minLatency {
            minLatency = latency
        }
    }
    
    averageLatency := totalLatency / time.Duration(len(latencies))
    requestsPerSec := float64(totalRequests) / duration.Seconds()
    
    return &LoadTestResult{
        TotalRequests:   totalRequests,
        SuccessRequests: successRequests,
        FailedRequests:  failedRequests,
        AverageLatency:  averageLatency,
        MaxLatency:      maxLatency,
        MinLatency:      minLatency,
        RequestsPerSec:  requestsPerSec,
    }
}

// 发送登录请求
func (lt *LoadTester) sendLoginRequest(workerID int) bool {
    loginData := map[string]string{
        "username": fmt.Sprintf("user%d", workerID%100), // 循环使用100个用户
        "password": "password123",
    }
    
    jsonData, _ := json.Marshal(loginData)
    
    resp, err := lt.client.Post(
        lt.config.BaseURL+"/api/v1/login",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    
    if err != nil {
        return false
    }
    defer resp.Body.Close()
    
    return resp.StatusCode == http.StatusOK
}

// 负载测试
func TestLoginLoadTest(t *testing.T) {
    config := LoadTestConfig{
        BaseURL:     "http://localhost:8080",
        Concurrency: 50,
        Duration:    30 * time.Second,
        RampUp:      5 * time.Second,
    }
    
    tester := NewLoadTester(config)
    result := tester.TestLoginEndpoint()
    
    t.Logf("负载测试结果:")
    t.Logf("总请求数: %d", result.TotalRequests)
    t.Logf("成功请求数: %d", result.SuccessRequests)
    t.Logf("失败请求数: %d", result.FailedRequests)
    t.Logf("平均延迟: %v", result.AverageLatency)
    t.Logf("最大延迟: %v", result.MaxLatency)
    t.Logf("最小延迟: %v", result.MinLatency)
    t.Logf("每秒请求数: %.2f", result.RequestsPerSec)
    
    // 性能断言
    if result.AverageLatency > 100*time.Millisecond {
        t.Errorf("平均延迟过高: %v", result.AverageLatency)
    }
    
    if result.RequestsPerSec < 100 {
        t.Errorf("吞吐量过低: %.2f req/s", result.RequestsPerSec)
    }
    
    successRate := float64(result.SuccessRequests) / float64(result.TotalRequests)
    if successRate < 0.99 {
        t.Errorf("成功率过低: %.2f%%", successRate*100)
    }
}

14.6 代码质量保证

14.6.1 代码覆盖率

#!/bin/bash
# scripts/test-coverage.sh

set -e

echo "运行测试并生成覆盖率报告..."

# 创建覆盖率目录
mkdir -p coverage

# 运行测试并生成覆盖率数据
go test -v -race -coverprofile=coverage/coverage.out ./...

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

# 显示覆盖率统计
echo "\n覆盖率统计:"
go tool cover -func=coverage/coverage.out

# 检查覆盖率阈值
COVERAGE=$(go tool cover -func=coverage/coverage.out | grep total | awk '{print $3}' | sed 's/%//')
THRESHOLD=80

echo "\n当前覆盖率: ${COVERAGE}%"
echo "最低要求: ${THRESHOLD}%"

if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
    echo "❌ 覆盖率低于阈值 ${THRESHOLD}%"
    exit 1
else
    echo "✅ 覆盖率达标"
fi

echo "\n覆盖率报告已生成: coverage/coverage.html"

14.6.2 静态代码分析

# .golangci.yml
linters-settings:
  govet:
    check-shadowing: true
  golint:
    min-confidence: 0
  gocyclo:
    min-complexity: 15
  maligned:
    suggest-new: true
  dupl:
    threshold: 100
  goconst:
    min-len: 2
    min-occurrences: 2
  depguard:
    list-type: blacklist
    packages:
      - github.com/sirupsen/logrus
    packages-with-error-message:
      - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log"
  misspell:
    locale: US
  lll:
    line-length: 140
  goimports:
    local-prefixes: your-project
  gocritic:
    enabled-tags:
      - diagnostic
      - experimental
      - opinionated
      - performance
      - style
    disabled-checks:
      - dupImport
      - ifElseChain
      - octalLiteral
      - whyNoLint
      - wrapperFunc

linters:
  disable-all: true
  enable:
    - bodyclose
    - deadcode
    - depguard
    - dogsled
    - dupl
    - errcheck
    - exhaustive
    - funlen
    - gochecknoinits
    - goconst
    - gocritic
    - gocyclo
    - gofmt
    - goimports
    - golint
    - gomnd
    - goprintffuncname
    - gosec
    - gosimple
    - govet
    - ineffassign
    - interfacer
    - lll
    - misspell
    - nakedret
    - noctx
    - nolintlint
    - rowserrcheck
    - scopelint
    - staticcheck
    - structcheck
    - stylecheck
    - typecheck
    - unconvert
    - unparam
    - unused
    - varcheck
    - whitespace

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - gomnd
        - funlen
        - goconst

run:
  timeout: 5m
  issues-exit-code: 1
  tests: true
  skip-dirs:
    - bin
    - vendor
    - var
    - tmp
  skip-files:
    - \.pb\.go$
    - \.gen\.go$

14.6.3 代码质量检查脚本

#!/bin/bash
# scripts/quality-check.sh

set -e

echo "🔍 开始代码质量检查..."

# 1. 格式化检查
echo "\n📝 检查代码格式..."
if ! gofmt -l . | grep -q .; then
    echo "✅ 代码格式正确"
else
    echo "❌ 代码格式不正确,请运行 'go fmt ./...'"
    gofmt -l .
    exit 1
fi

# 2. 导入排序检查
echo "\n📦 检查导入排序..."
if ! goimports -l . | grep -q .; then
    echo "✅ 导入排序正确"
else
    echo "❌ 导入排序不正确,请运行 'goimports -w .'"
    goimports -l .
    exit 1
fi

# 3. 静态分析
echo "\n🔬 运行静态分析..."
if command -v golangci-lint &> /dev/null; then
    golangci-lint run
    echo "✅ 静态分析通过"
else
    echo "⚠️  golangci-lint 未安装,跳过静态分析"
fi

# 4. 安全检查
echo "\n🔒 运行安全检查..."
if command -v gosec &> /dev/null; then
    gosec ./...
    echo "✅ 安全检查通过"
else
    echo "⚠️  gosec 未安装,跳过安全检查"
fi

# 5. 依赖检查
echo "\n📋 检查依赖..."
go mod tidy
if git diff --quiet go.mod go.sum; then
    echo "✅ 依赖文件正确"
else
    echo "❌ 依赖文件需要更新,请运行 'go mod tidy'"
    exit 1
fi

# 6. 构建检查
echo "\n🔨 检查构建..."
go build ./...
echo "✅ 构建成功"

# 7. 测试检查
echo "\n🧪 运行测试..."
go test -race -short ./...
echo "✅ 测试通过"

echo "\n🎉 代码质量检查完成!"

14.6.4 持续集成配置

# .github/workflows/quality.yml
name: Code Quality

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  quality:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:6
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.21
    
    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-
    
    - name: Install dependencies
      run: |
        go mod download
        go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
        go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
    
    - name: Run quality checks
      run: ./scripts/quality-check.sh
    
    - name: Run tests with coverage
      run: |
        go test -v -race -coverprofile=coverage.out ./...
        go tool cover -html=coverage.out -o coverage.html
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.out
        flags: unittests
        name: codecov-umbrella
    
    - name: Upload coverage reports
      uses: actions/upload-artifact@v3
      with:
        name: coverage-report
        path: coverage.html
    
    - name: Run integration tests
      env:
        DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable
        REDIS_URL: redis://localhost:6379/0
      run: |
        go test -tags=integration -v ./test/integration/...
    
    - name: Run benchmark tests
      run: |
        go test -bench=. -benchmem ./...

14.7 测试数据管理

14.7.1 测试数据工厂

package testutil

import (
    "time"
    
    "github.com/bxcodec/faker/v3"
    "your-project/model"
)

// 用户工厂
type UserFactory struct{}

func NewUserFactory() *UserFactory {
    return &UserFactory{}
}

func (f *UserFactory) Build() *model.User {
    return &model.User{
        Username: faker.Username(),
        Email:    faker.Email(),
        Password: "$2a$10$hashedpassword", // 预哈希密码
        Status:   model.UserStatusActive,
    }
}

func (f *UserFactory) BuildWithUsername(username string) *model.User {
    user := f.Build()
    user.Username = username
    return user
}

func (f *UserFactory) BuildBatch(count int) []*model.User {
    users := make([]*model.User, count)
    for i := 0; i < count; i++ {
        users[i] = f.Build()
    }
    return users
}

// 令牌工厂
type TokenFactory struct{}

func NewTokenFactory() *TokenFactory {
    return &TokenFactory{}
}

func (f *TokenFactory) Build() *model.Token {
    return &model.Token{
        Key:         "sk-" + faker.UUIDDigit(),
        Name:        faker.Name(),
        Type:        model.TokenTypeAccess,
        Status:      model.TokenStatusEnabled,
        RemainQuota: 1000000,
        UsedQuota:   0,
        ExpiredTime: time.Now().Add(365 * 24 * time.Hour),
    }
}

func (f *TokenFactory) BuildForUser(userID uint) *model.Token {
    token := f.Build()
    token.UserID = userID
    return token
}

func (f *TokenFactory) BuildWithQuota(quota int64) *model.Token {
    token := f.Build()
    token.RemainQuota = quota
    return token
}

// 渠道工厂
type ChannelFactory struct{}

func NewChannelFactory() *ChannelFactory {
    return &ChannelFactory{}
}

func (f *ChannelFactory) Build() *model.Channel {
    return &model.Channel{
        Name:     faker.Name(),
        Type:     1, // OpenAI类型
        Key:      "sk-" + faker.UUIDDigit(),
        BaseURL:  "https://api.openai.com/v1",
        Models:   "gpt-3.5-turbo,gpt-4",
        Status:   model.ChannelStatusEnabled,
        Priority: 1,
        Weight:   100,
    }
}

func (f *ChannelFactory) BuildWithType(channelType int) *model.Channel {
    channel := f.Build()
    channel.Type = channelType
    return channel
}

// 测试数据管理器
type TestDataManager struct {
    db          *gorm.DB
    userFactory *UserFactory
    tokenFactory *TokenFactory
    channelFactory *ChannelFactory
}

func NewTestDataManager(db *gorm.DB) *TestDataManager {
    return &TestDataManager{
        db:             db,
        userFactory:    NewUserFactory(),
        tokenFactory:   NewTokenFactory(),
        channelFactory: NewChannelFactory(),
    }
}

// 创建测试用户
func (m *TestDataManager) CreateUser() *model.User {
    user := m.userFactory.Build()
    m.db.Create(user)
    return user
}

// 创建测试用户和令牌
func (m *TestDataManager) CreateUserWithToken() (*model.User, *model.Token) {
    user := m.CreateUser()
    token := m.tokenFactory.BuildForUser(user.ID)
    m.db.Create(token)
    return user, token
}

// 创建测试渠道
func (m *TestDataManager) CreateChannel() *model.Channel {
    channel := m.channelFactory.Build()
    m.db.Create(channel)
    return channel
}

// 清理测试数据
func (m *TestDataManager) Cleanup() {
    m.db.Exec("DELETE FROM tokens")
    m.db.Exec("DELETE FROM channels")
    m.db.Exec("DELETE FROM users")
}

14.7.2 测试数据库管理

package testutil

import (
    "fmt"
    "os"
    "testing"
    
    "gorm.io/driver/postgres"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    
    "your-project/model"
)

// 测试数据库配置
type TestDBConfig struct {
    Driver   string
    Host     string
    Port     int
    User     string
    Password string
    DBName   string
}

// 获取测试数据库配置
func GetTestDBConfig() *TestDBConfig {
    driver := os.Getenv("TEST_DB_DRIVER")
    if driver == "" {
        driver = "sqlite" // 默认使用SQLite
    }
    
    return &TestDBConfig{
        Driver:   driver,
        Host:     getEnv("TEST_DB_HOST", "localhost"),
        Port:     getEnvInt("TEST_DB_PORT", 5432),
        User:     getEnv("TEST_DB_USER", "postgres"),
        Password: getEnv("TEST_DB_PASSWORD", "postgres"),
        DBName:   getEnv("TEST_DB_NAME", "testdb"),
    }
}

// 创建测试数据库连接
func SetupTestDB(t *testing.T) *gorm.DB {
    config := GetTestDBConfig()
    
    var db *gorm.DB
    var err error
    
    gormConfig := &gorm.Config{
        Logger: logger.Default.LogMode(logger.Silent), // 测试时静默日志
    }
    
    switch config.Driver {
    case "postgres":
        dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
            config.Host, config.Port, config.User, config.Password, config.DBName)
        db, err = gorm.Open(postgres.Open(dsn), gormConfig)
    case "sqlite":
        db, err = gorm.Open(sqlite.Open(":memory:"), gormConfig)
    default:
        t.Fatalf("不支持的数据库驱动: %s", config.Driver)
    }
    
    if err != nil {
        t.Fatalf("连接测试数据库失败: %v", err)
    }
    
    // 自动迁移
    err = db.AutoMigrate(
        &model.User{},
        &model.Token{},
        &model.Channel{},
        &model.Log{},
    )
    if err != nil {
        t.Fatalf("数据库迁移失败: %v", err)
    }
    
    return db
}

// 清理测试数据库
func CleanupTestDB(db *gorm.DB) {
    // 按依赖关系顺序删除
    db.Exec("DELETE FROM logs")
    db.Exec("DELETE FROM tokens")
    db.Exec("DELETE FROM channels")
    db.Exec("DELETE FROM users")
}

// 辅助函数
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
    if value := os.Getenv(key); value != "" {
        if intValue, err := strconv.Atoi(value); err == nil {
            return intValue
        }
    }
    return defaultValue
}

14.8 本章小结

本章详细介绍了企业级Go应用的测试与质量保证体系:

14.8.1 测试体系构建

  1. 测试金字塔模型

    • 大量单元测试作为基础

    • 适量集成测试验证组件协作

    • 少量端到端测试覆盖关键流程

  2. 测试分类与策略

    • 单元测试:快速反馈,高覆盖率

    • 集成测试:验证系统集成点

    • 端到端测试:模拟真实用户场景

    • 性能测试:确保系统性能指标

14.8.2 测试实践要点

  1. 单元测试最佳实践

    • 使用测试套件组织测试

    • Mock外部依赖

    • 测试用例设计要全面

    • 断言要精确和有意义

  2. 集成测试关键点

    • 使用真实的外部服务

    • 测试数据隔离

    • 环境一致性保证

    • 清理测试数据

  3. 性能测试要素

    • 基准测试建立性能基线

    • 负载测试验证系统容量

    • 并发测试检查竞态条件

    • 性能指标监控和分析

14.8.3 质量保证措施

  1. 代码质量检查

    • 静态代码分析

    • 代码覆盖率监控

    • 安全漏洞扫描

    • 依赖管理检查

  2. 持续集成流程

    • 自动化测试执行

    • 质量门禁设置

    • 测试报告生成

    • 失败快速反馈

14.8.4 测试工具链

  1. Go原生工具

    • testing包:基础测试框架

    • go test:测试执行器

    • go tool cover:覆盖率工具

  2. 第三方工具

    • testify:增强断言和Mock

    • golangci-lint:静态分析

    • gosec:安全检查

    • Playwright:端到端测试

通过建立完善的测试体系和质量保证流程,可以显著提高代码质量,减少生产环境问题,提升开发效率和用户满意度。

14.9 练习题

  1. 单元测试编写

    • 为渠道服务编写完整的单元测试

    • 包括正常场景和异常场景

    • 使用Mock模拟外部依赖

  2. 集成测试设计

    • 设计用户注册到令牌使用的完整集成测试

    • 包括数据库操作和缓存操作

    • 验证业务流程的正确性

  3. 性能测试实现

    • 实现API接口的负载测试

    • 设置合理的性能指标阈值

    • 分析性能瓶颈并提出优化建议

  4. 质量保证流程

    • 配置完整的CI/CD流水线

    • 设置代码质量门禁

    • 实现自动化测试报告

14.10 扩展阅读

官方文档

测试框架和库

性能测试

代码质量

技术书籍

  1. 《Go语言测试驱动开发》 - TDD实践指南

  2. 《持续集成与持续部署》 - CI/CD最佳实践

  3. 《软件测试的艺术》 - 经典测试理论

  4. 《性能测试实战》 - 性能测试深度指南

  5. 《Google软件测试之道》 - Google测试文化

  6. 《单元测试的艺术》 - 单元测试最佳实践

  7. 《微服务测试》 - 微服务架构测试策略

在线资源

开源项目参考

通过本章的学习和实践,你应该能够建立完整的Go语言测试体系,确保代码质量,提高开发效率和系统可靠性。

最后更新于

这有帮助吗?