第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 测试金字塔模型
测试金字塔的核心原则
基础稳固原则:单元测试作为金字塔底层,应该覆盖大部分代码逻辑
成本效益原则:越往上层,测试成本越高,执行时间越长
反馈速度原则:底层测试提供快速反馈,顶层测试提供全面验证
维护成本原则:单元测试易于维护,UI测试维护成本最高
New-API项目中的测试分布
pie title 测试类型分布
"单元测试" : 65
"集成测试" : 25
"端到端测试" : 10图2 New-API项目测试分布
14.1.2 测试分类
按测试范围分类
单元测试(Unit Testing)
定义:测试单个函数、方法或类的最小可测试单元
特点:执行速度快(毫秒级),反馈及时,易于调试
覆盖率要求:核心业务逻辑 >80%,工具类 >70%
适用场景:业务逻辑验证、边界条件测试、异常处理
集成测试(Integration Testing)
定义:测试多个模块或组件之间的交互和协作
类型:
组件集成测试:测试内部模块间交互
系统集成测试:测试与外部系统(数据库、缓存、第三方API)的集成
验证重点:数据流转、接口契约、错误传播
端到端测试(E2E Testing)
定义:从用户角度测试完整的业务流程
特点:模拟真实用户操作,验证系统整体功能
执行环境:接近生产环境的测试环境
关注点:用户体验、业务流程完整性
按测试目的分类
功能测试(Functional Testing)
验证系统功能是否符合需求规格
包括正向测试和负向测试
性能测试(Performance Testing)
负载测试:验证系统在预期负载下的性能表现
压力测试:测试系统在极限负载下的稳定性
基准测试:建立性能基线,用于性能回归检测
安全测试(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(), ®isterResult)
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 测试体系构建
测试金字塔模型
大量单元测试作为基础
适量集成测试验证组件协作
少量端到端测试覆盖关键流程
测试分类与策略
单元测试:快速反馈,高覆盖率
集成测试:验证系统集成点
端到端测试:模拟真实用户场景
性能测试:确保系统性能指标
14.8.2 测试实践要点
单元测试最佳实践
使用测试套件组织测试
Mock外部依赖
测试用例设计要全面
断言要精确和有意义
集成测试关键点
使用真实的外部服务
测试数据隔离
环境一致性保证
清理测试数据
性能测试要素
基准测试建立性能基线
负载测试验证系统容量
并发测试检查竞态条件
性能指标监控和分析
14.8.3 质量保证措施
代码质量检查
静态代码分析
代码覆盖率监控
安全漏洞扫描
依赖管理检查
持续集成流程
自动化测试执行
质量门禁设置
测试报告生成
失败快速反馈
14.8.4 测试工具链
Go原生工具
testing包:基础测试框架
go test:测试执行器
go tool cover:覆盖率工具
第三方工具
testify:增强断言和Mock
golangci-lint:静态分析
gosec:安全检查
Playwright:端到端测试
通过建立完善的测试体系和质量保证流程,可以显著提高代码质量,减少生产环境问题,提升开发效率和用户满意度。
14.9 练习题
单元测试编写
为渠道服务编写完整的单元测试
包括正常场景和异常场景
使用Mock模拟外部依赖
集成测试设计
设计用户注册到令牌使用的完整集成测试
包括数据库操作和缓存操作
验证业务流程的正确性
性能测试实现
实现API接口的负载测试
设置合理的性能指标阈值
分析性能瓶颈并提出优化建议
质量保证流程
配置完整的CI/CD流水线
设置代码质量门禁
实现自动化测试报告
14.10 扩展阅读
官方文档
测试框架和库
性能测试
代码质量
技术书籍
《Go语言测试驱动开发》 - TDD实践指南
《持续集成与持续部署》 - CI/CD最佳实践
《软件测试的艺术》 - 经典测试理论
《性能测试实战》 - 性能测试深度指南
《Google软件测试之道》 - Google测试文化
《单元测试的艺术》 - 单元测试最佳实践
《微服务测试》 - 微服务架构测试策略
在线资源
开源项目参考
通过本章的学习和实践,你应该能够建立完整的Go语言测试体系,确保代码质量,提高开发效率和系统可靠性。
最后更新于
这有帮助吗?
