问题修复: - 修复 TimeoutMiddleware 死锁问题(嵌套锁调用) - 修复竞态条件(responseSent 标志确保只发送一次响应) - 基准测试超时从 5ms 改为 100ms 避免 race 检测不稳定 文档更新: - 添加中间件并发测试要点(testing_strategy_v1.md) - 添加 TimeoutMiddleware 并发安全经验(project_experience_summary.md) - 更新测试覆盖率报告 - 新建项目状态报告
16 KiB
16 KiB
Supply API 测试方案 v1.2
1. 概述
本文档定义了 Supply API 项目的系统性测试策略,确保代码质量、可维护性和快速迭代能力。 遵循 Google Testing Blog 和 Atlassian Testing Guide 行业最佳实践。
2. 测试金字塔(标准三层)
2.1 金字塔结构
┌─────────────┐
│ E2E │ ← 5-10% (Playwright)
┌─────────────┐
│ Integration │ ← 15-20% (Store, DB, 真实依赖)
┌───────────────┐
│ Unit │ ← 70-80% (业务逻辑、领域模型)
└───────────────┘
2.2 各层定义
| 层级 | 目标占比 | 定义 | Build Tag | 速度目标 |
|---|---|---|---|---|
| E2E | 5-10% | 关键业务流程端到端验证 | //go:build e2e |
< 1s |
| Integration | 15-20% | Store、Repository、DB 集成 | //go:build integration |
< 100ms |
| Unit | 70-80% | 业务逻辑、领域模型、组件 | (默认) | < 10ms |
注意: 移除非标准的 "Component" 层,其包含在 Unit 层中。
3. 测试组织结构
3.1 文件命名规范
{package}_test.go # 单元测试(默认)
{package}_integration_test.go # 集成测试(需数据库)
{package}_e2e_test.go # E2E 测试(需完整环境)
{package}_slow_test.go # 慢速测试(默认跳过)
3.2 Build Tag 使用
//go:build unit
// +build unit
package domain_test // 单元测试
//go:build integration
// +build integration
package repository_test // 集成测试
//go:build e2e
// +build e2e
package e2e_test // E2E 测试
//go:build slow
// +build slow
package slow_test // 慢速测试(CI中默认跳过)
3.3 测试包结构
internal/
├── domain/ # 领域模型
│ ├── account.go
│ ├── account_test.go # 账号单元测试
│ ├── package.go
│ ├── package_test.go # 套餐单元测试
│ └── invariants_test.go # 不变量测试
│
├── testutil/ # 测试工具包(新增)
│ ├── factory/ # 测试数据工厂
│ │ ├── account.go
│ │ ├── package.go
│ │ └── settlement.go
│ ├── mock/ # 统一Mock
│ │ └── mocks.go
│ └── assert/ # 自定义断言
│ └── assertions.go
│
├── middleware/ # HTTP中间件
│ ├── auth.go
│ └── auth_test.go # 认证测试
4. 测试数据管理
4.1 测试数据工厂(新增)
// internal/testutil/factory/account.go
type AccountFactory struct {
supplierID int64
provider Provider
accountType AccountType
credential string
riskAck bool
}
func NewAccountFactory() *AccountFactory {
return &AccountFactory{
supplierID: 1001,
provider: ProviderOpenAI,
accountType: AccountTypeAPIKey,
credential: "sk-test-key",
riskAck: true,
}
}
func (f *AccountFactory) WithSupplierID(id int64) *AccountFactory {
f.supplierID = id
return f
}
func (f *AccountFactory) WithProvider(p Provider) *AccountFactory {
f.provider = p
return f
}
func (f *AccountFactory) Build() *CreateAccountRequest {
return &CreateAccountRequest{
SupplierID: f.supplierID,
Provider: f.provider,
AccountType: f.accountType,
Credential: f.credential,
RiskAck: f.riskAck,
}
}
// 使用示例
func TestAccountService_Create(t *testing.T) {
factory := NewAccountFactory()
// 正常场景
req := factory.Build()
// 边界场景
invalidReq := factory.
WithCredential("").
Build()
}
4.2 固定测试数据
func TestAccountService_Create(t *testing.T) {
store := newMockAccountStore()
req := &CreateAccountRequest{
SupplierID: 1001,
Provider: ProviderOpenAI,
AccountType: AccountTypeAPIKey,
Credential: "sk-test-key",
RiskAck: true,
}
account, err := store.Create(context.Background(), req)
// ...
}
4.3 边界值测试
tests := []struct {
name string
input float64
want bool
}{
{"zero", 0.0, true},
{"positive", 100.0, true},
{"negative", -1.0, false},
{"very large", 1e10, true},
}
5. 单元测试规范
5.1 测试结构 (AAA模式)
func TestXXX_Scenario(t *testing.T) {
// Arrange - 准备测试数据
store := newMockStore()
svc := NewService(store)
// Act - 执行被测操作
result, err := svc.DoSomething(ctx, req)
// Assert - 验证结果
assert.NoError(t, err)
assert.Equal(t, expected, result)
}
5.2 Mock 接口而非具体实现
// ✅ 正确 - Mock 接口
type mockSettlementStore struct {
settlements map[int64]*Settlement
}
func (m *mockSettlementStore) GetByID(ctx context.Context, supplierID, id int64) (*Settlement, error) {
if s, ok := m.settlements[id]; ok && s.SupplierID == supplierID {
return s, nil
}
return nil, errors.New("not found")
}
// ❌ 错误 - Mock 具体类型
type mockRepo struct {
repo *repository.SettlementRepository
}
5.3 Mock 审计存储正确姿势
type AuditStore interface {
Emit(ctx context.Context, event audit.Event) error
Query(ctx context.Context, filter audit.EventFilter) ([]audit.Event, error)
QueryWithTotal(ctx context.Context, filter audit.EventFilter) ([]audit.Event, int64, error)
GetByID(ctx context.Context, eventID string) (audit.Event, error)
}
// ✅ 正确 - 使用具体类型
func (m *mockAuditStore) Emit(ctx context.Context, event audit.Event) error {
return nil
}
// ✅ 错误模拟 - 返回错误
func (m *mockFailingAuditStore) Emit(ctx context.Context, event audit.Event) error {
return errors.New("audit emit failed")
}
5.4 表驱动测试
func TestSettlementStatus_Transitions(t *testing.T) {
tests := []struct {
name string
from SettlementStatus
to SettlementStatus
expected bool
}{
{"pending to processing", SettlementStatusPending, SettlementStatusProcessing, true},
{"pending to completed", SettlementStatusPending, SettlementStatusCompleted, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateStateTransition(tt.from, tt.to)
assert.Equal(t, tt.expected, result)
})
}
}
6. 集成测试规范
6.1 Build Tag 隔离
//go:build integration
// +build integration
package repository_test
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIntegrationSettlementRepository(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// 需要真实的 PostgreSQL 或使用 sqlmock
}
6.2 使用 Test Database
//go:build integration
// +build integration
func TestIntegrationSettlementRepository(t *testing.T) {
// 选项1: 使用 sqlmock
db, mock, _ := sqlmock.New()
defer db.Close()
// 选项2: 使用轻量级测试数据库
// 推荐: github.com/testcontainers/testcontainers-go
}
6.3 运行命令
# 只运行单元测试(默认)
go test ./...
# 包含集成测试
go test -tags=integration ./...
# 排除集成测试(快速模式)
go test -short ./...
# 运行慢速测试
go test -tags=slow ./...
7. 覆盖率要求
7.1 模块覆盖率目标
| 模块 | 最低覆盖率 | 当前 | 状态 |
|---|---|---|---|
| domain | 70% | 71.2% | ✅ |
| middleware | 80% | 80.4% | ✅ |
| audit/handler | 75% | 79.6% | ✅ |
| audit/service | 80% | 83.0% | ✅ |
| audit/model | 80% | 93.8% | ✅ |
| audit/sanitizer | 80% | 84.3% | ✅ |
| security | 80% | 88.8% | ✅ |
| iam | 70% | 93.2% | ✅ |
7.2 覆盖率检查命令
# ✅ 推荐:单独验证关键模块(显示真实覆盖率)
go test -cover ./internal/domain/... # → 71.2%
go test -cover ./internal/middleware/... # → 80.4%
# ⚠️ 联合运行(覆盖率数值会被稀释)
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
7.3 覆盖率未达标处理
- 分析未覆盖代码路径
- 添加针对性测试用例
- 确认覆盖率达到目标
- 禁止强行凑覆盖率而编写无意义测试
8. 测试命名规范
8.1 函数命名
Test{Service}_{Method}_{Scenario}
示例:
- TestAccountService_Create_Success
- TestAccountService_Create_InvalidInput
- TestPackageService_Publish_ExpiredPackage
- TestSettlementService_Withdraw_ExceedsBalance
8.2 子测试命名
func TestAccountService_Activate(t *testing.T) {
tests := []struct {
name string
setup func() *Account
supplierID int64
wantErr bool
}{
{
name: "activate pending account success",
setup: func() *Account { /* ... */ },
supplierID: 1001,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// ...
})
}
}
9. 并发与竞态测试
9.1 启用 Race 检测
# 运行所有测试并检测竞态条件
go test -race ./...
# 详细输出
go test -race -v ./internal/domain/...
9.2 并发安全测试示例
func TestConcurrentAccountAccess(t *testing.T) {
store := newMockAccountStore()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
_, err := store.GetByID(context.Background(), 1001, 1)
assert.NoError(t, err)
}(i)
}
wg.Wait()
}
9.2 中间件并发测试要点
中间件的并发安全问题通常体现在:
- ResponseWriter 的并发写入
- 共享状态的竞争访问
- 超时与正常响应的冲突
TimeoutMiddleware 正确测试模式:
// ✅ 正确:超时设置足够长(>100ms),确保正常完成
func TestWithTimeoutMiddleware_NormalCompletion(t *testing.T) {
handler := WithTimeoutMiddleware(nextHandler, 100*time.Millisecond)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
// ✅ 正确:使用 WaitGroup 确保 handler 完成后再检查
func TestWithTimeoutMiddleware_Concurrent(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
handler.ServeHTTP(w, req)
}()
wg.Wait()
// 现在可以安全检查结果
}
⚠️ 常见错误:
// ❌ 错误:超时设置过短(<10ms)导致 race 检测下不稳定
timeoutHandler := WithTimeoutMiddleware(handler, 1*time.Millisecond)
// ❌ 错误:测试并发写入 ResponseRecorder 但不等待完成
go func() {
handler.ServeHTTP(w, req) // 可能还在执行
}()
time.Sleep(10 * time.Millisecond)
assert.Equal(t, 200, w.Code) // w 可能尚未写入
// ❌ 错误:假设 select 会优先选择已关闭的 channel
// 当 handlerDone 和 timeout 同时就绪时,行为是未定义的
9.3 Race 检测必须通过
所有并发测试必须在 race 模式下通过:
# 必须验证
go test -race ./internal/middleware/...
# 基准测试也需要 race 检测
go test -race -bench=. ./internal/benchmark/...
10. 性能回归测试
10.1 执行时间监控
//go:build slow
// +build slow
func TestPerformance_SettlementQuery(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test")
}
start := time.Now()
// 执行查询
result, err := svc.Query(ctx, req)
elapsed := time.Since(start)
// 断言在可接受范围内
assert.NoError(t, err)
assert.True(t, elapsed < 100*time.Millisecond,
"Query took %v, expected < 100ms", elapsed)
}
11. 测试运行策略
11.1 本地开发
# 快速测试(跳过慢速和集成测试)
go test -short ./...
# 完整测试(含集成测试)
go test -tags=integration, slow ./...
# 竞态检测
go test -race ./...
# 只测试修改的包
go test ./internal/domain/...
# 详细输出
go test -v -cover ./internal/domain/...
11.2 CI/CD
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run unit tests
run: go test -short -race -coverprofile=coverage.out ./...
- name: Run integration tests
run: go test -tags=integration -race -coverprofile=coverage.out ./...
- name: Run slow tests
run: go test -tags=slow ./...
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
12. 常见问题处理
12.1 测试依赖外部服务
// ✅ 使用 Mock
func TestSettlementService(t *testing.T) {
store := newMockSettlementStore()
svc := NewSettlementService(store, nil, nil)
}
12.2 时间相关测试
// 使用依赖注入
type SettlementService struct {
store SettlementStore
clock Clock // 注入时间依赖
}
12.3 Flaky 测试处理
// ❌ 错误 - 在测试中重试
func TestNetworkCall(t *testing.T) {
for i := 0; i < 3; i++ {
if err := attempt(); err == nil {
return
}
}
}
// ✅ 正确 - 标记为已知问题并使用超时
func TestNetworkCall(t *testing.T) {
if os.Getenv("CI") == "" {
t.Skip("Skipping flaky test outside CI")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := callWithRetry(ctx, endpoint)
assert.NoError(t, err)
}
13. 测试检查清单
新代码合并前:
- 所有单元测试通过 (
go test ./...) - 覆盖率达标(无下降)
- Race 检测通过 (
go test -race ./...) - 无
TODO或FIXME遗留测试 - Mock 使用正确接口签名
- 测试名称符合规范
- 表驱动测试覆盖边界情况
- 集成测试在 CI 中正常运行
- 性能测试在慢速测试套件中
14. 下一步行动计划
✅ 已完成
- Domain 模块覆盖率提升 (40.7% → 71.2%)
- Middleware 模块覆盖率提升 (52.7% → 80.4%)
- Audit handler 模块覆盖率提升 (75% → 79.6%)
P1 - 创建测试工具包
- testutil/factory - 测试数据工厂
- testutil/mock - 统一Mock库
- testutil/assert - 自定义断言
P2 - 完善集成测试
- Repository 模块集成测试骨架
- Settlement Store 集成测试
P3 - 补充测试类型
- E2E 测试骨架
- 性能回归测试