From 4349666ccb3a871f243cacb9450fc8c264e956c1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 8 Apr 2026 10:23:13 +0800 Subject: [PATCH] docs: enhance testing strategy to v1.2 with industry best practices Based on expert review, key improvements: 1. Standardize testing pyramid to 3 layers (Unit/Integration/E2E) - Remove non-standard "Component" layer - Add target percentages per industry standards 2. Add test utilities infrastructure - testutil/factory/ - Test data factories - testutil/mock/ - Unified mock library - testutil/assert/ - Custom assertions 3. Add missing build tags - //go:build slow for performance tests - //go:build e2e for E2E tests 4. Add performance regression testing guidelines 5. Fix flaky test handling - Proper use of context timeout - Skip flaky tests in local dev, run in CI 6. Update references to Google Testing Blog and Atlassian Testing Guide Coverage targets remain aligned with industry: - Unit: 70-80% - Integration: 15-20% - E2E: 5-10% --- supply-api/docs/testing_strategy_v1.md | 543 ++++++++++++++----------- 1 file changed, 312 insertions(+), 231 deletions(-) diff --git a/supply-api/docs/testing_strategy_v1.md b/supply-api/docs/testing_strategy_v1.md index 1a1537d2..0f01d653 100644 --- a/supply-api/docs/testing_strategy_v1.md +++ b/supply-api/docs/testing_strategy_v1.md @@ -1,45 +1,50 @@ -# Supply API 测试方案 v1.1 +# Supply API 测试方案 v1.2 ## 1. 概述 本文档定义了 Supply API 项目的系统性测试策略,确保代码质量、可维护性和快速迭代能力。 - -### 1.1 测试金字塔 - -``` - ┌─────────────┐ - │ E2E │ ← 关键业务流程验证 - ┌─────────────┐ - │ Integration│ ← API、DB、消息队列 - ┌───────────────┐ - │ Unit │ ← 业务逻辑、领域模型 - ┌─────────────────┐ - │ Component │ ← 单组件内部逻辑 - └──────────────────┘ -``` - -| 层级 | 目标 | 工具 | 速度 | Build Tag | -|------|------|------|------|-----------| -| E2E | 关键业务流程 | Playwright | < 1s | `//go:build e2e` | -| Integration | Store、Repository、DB | go:build integration | < 100ms | `//go:build integration` | -| Unit | 业务逻辑、领域模型 | Go testing + testify | < 1ms | (默认) | -| Component | 单组件内部逻辑 | Go testing | < 1ms | (默认) | - -**重要**: Middleware 模块当前覆盖率 52.7%,**未达标**(目标 80%),需优先改进。 +遵循 Google Testing Blog 和 Atlassian Testing Guide 行业最佳实践。 --- -## 2. 测试组织结构 +## 2. 测试金字塔(标准三层) -### 2.1 文件命名规范 +### 2.1 金字塔结构 ``` -{package}_test.go # 单元测试(默认,无 build tag) + ┌─────────────┐ + │ 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 # 慢速测试(默认跳过) ``` -### 2.2 Build Tag 使用 +### 3.2 Build Tag 使用 ```go //go:build unit @@ -56,143 +61,97 @@ package repository_test // 集成测试 // +build e2e package e2e_test // E2E 测试 + +//go:build slow +// +build slow + +package slow_test // 慢速测试(CI中默认跳过) ``` -### 2.3 测试包结构 +### 3.3 测试包结构 ``` internal/ ├── domain/ # 领域模型 -│ ├── account.go # 账号领域逻辑 +│ ├── account.go │ ├── account_test.go # 账号单元测试 -│ ├── package.go # 套餐领域逻辑 +│ ├── package.go │ ├── package_test.go # 套餐单元测试 │ └── invariants_test.go # 不变量测试 │ -├── audit/ # 审计模块 -│ ├── service/ -│ │ ├── audit_service.go -│ │ └── audit_service_test.go -│ └── handler/ -│ ├── audit_handler.go -│ └── audit_handler_test.go +├── testutil/ # 测试工具包(新增) +│ ├── factory/ # 测试数据工厂 +│ │ ├── account.go +│ │ ├── package.go +│ │ └── settlement.go +│ ├── mock/ # 统一Mock +│ │ └── mocks.go +│ └── assert/ # 自定义断言 +│ └── assertions.go │ ├── middleware/ # HTTP中间件 │ ├── auth.go -│ ├── auth_test.go # 认证测试 -│ ├── ratelimit.go -│ └── ratelimit_test.go # 限流测试 -``` - ---- - -## 3. 单元测试规范 - -### 3.1 测试结构 (AAA模式) - -```go -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) -} -``` - -### 3.2 Mock 接口而非具体实现 - -```go -// ✅ 正确 - 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 -} -``` - -### 3.3 Mock 审计存储正确姿势 - -审计存储使用 `audit.AuditStore` 接口: - -```go -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) -} -``` - -**成功场景:** -```go -func (m *mockAuditStore) Emit(ctx context.Context, event audit.Event) error { - return nil // 成功时不记录 -} -``` - -**错误场景(关键):** -```go -func (m *mockFailingAuditStore) Emit(ctx context.Context, event audit.Event) error { - return errors.New("audit emit failed") -} -``` - -### 3.4 表驱动测试 - -适用于多场景测试: - -```go -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) - }) - } -} +│ └── auth_test.go # 认证测试 ``` --- ## 4. 测试数据管理 -### 4.1 测试 Setup/Teardown +### 4.1 测试数据工厂(新增) ```go -func TestAccountService(t *testing.T) { - store := newMockAccountStore() +// internal/testutil/factory/account.go - t.Cleanup(func() { - // 清理测试数据(如果需要) - }) +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() } ``` @@ -232,9 +191,95 @@ tests := []struct { --- -## 5. 集成测试规范 +## 5. 单元测试规范 -### 5.1 Build Tag 隔离 +### 5.1 测试结构 (AAA模式) + +```go +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 接口而非具体实现 + +```go +// ✅ 正确 - 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 审计存储正确姿势 + +```go +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 表驱动测试 + +```go +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 //go:build integration @@ -251,11 +296,27 @@ func TestIntegrationSettlementRepository(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } - // 需要真实的 PostgreSQL + // 需要真实的 PostgreSQL 或使用 sqlmock } ``` -### 5.2 运行命令 +### 6.2 使用 Test Database + +```go +//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 运行命令 ```bash # 只运行单元测试(默认) @@ -264,52 +325,43 @@ go test ./... # 包含集成测试 go test -tags=integration ./... -# 排除集成测试 +# 排除集成测试(快速模式) go test -short ./... -# 运行特定 tag -go test -tags=unit ./internal/domain/... +# 运行慢速测试 +go test -tags=slow ./... ``` --- -## 6. 覆盖率要求 +## 7. 覆盖率要求 -### 6.1 模块覆盖率目标 +### 7.1 模块覆盖率目标 -| 模块 | 最低覆盖率 | 当前覆盖率 | 状态 | 优先级 | -|------|-----------|-----------|------|--------| -| domain | 70% | 71.2% | ✅ | - | -| **middleware** | **80%** | **52.7%** | 🔴 | **P0** | -| 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% | ✅ | - | +| 模块 | 最低覆盖率 | 当前 | 状态 | +|------|-----------|------|------| +| 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% | ✅ | -**⚠️ 关键问题**: Middleware 模块覆盖率 52.7%,与目标差距 -27.3%,需优先改进。 - -### 6.2 覆盖率检查命令 - -**重要**: Go test 在运行 `go test ./...` 时会进行覆盖率聚合,可能导致某些模块显示的覆盖率低于单独运行时的值。 +### 7.2 覆盖率检查命令 ```bash # ✅ 推荐:单独验证关键模块(显示真实覆盖率) go test -cover ./internal/domain/... # → 71.2% -go test -cover ./internal/middleware/... # → 80.4% -go test -cover ./internal/audit/handler/... -go test -cover ./internal/audit/service/... +go test -cover ./internal/middleware/... # → 80.4% -# ⚠️ 联合运行(覆盖率数值会被稀释,不反映真实情况) +# ⚠️ 联合运行(覆盖率数值会被稀释) go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html - -# 检查覆盖率达标情况(使用单独运行) -go test -cover ./internal/domain/... 2>&1 | grep "coverage" ``` -### 6.3 覆盖率未达标处理 +### 7.3 覆盖率未达标处理 1. 分析未覆盖代码路径 2. 添加针对性测试用例 @@ -318,9 +370,9 @@ go test -cover ./internal/domain/... 2>&1 | grep "coverage" --- -## 7. 测试命名规范 +## 8. 测试命名规范 -### 7.1 函数命名 +### 8.1 函数命名 ``` Test{Service}_{Method}_{Scenario} @@ -332,7 +384,7 @@ Test{Service}_{Method}_{Scenario} - TestSettlementService_Withdraw_ExceedsBalance ``` -### 7.2 子测试命名 +### 8.2 子测试命名 ```go func TestAccountService_Activate(t *testing.T) { @@ -348,12 +400,6 @@ func TestAccountService_Activate(t *testing.T) { supplierID: 1001, wantErr: false, }, - { - name: "activate non-existent fails", - setup: func() *Account { return nil }, - supplierID: 9999, - wantErr: true, - }, } for _, tt := range tests { @@ -366,9 +412,9 @@ func TestAccountService_Activate(t *testing.T) { --- -## 8. 并发与竞态测试 +## 9. 并发与竞态测试 -### 8.1 启用 Race 检测 +### 9.1 启用 Race 检测 ```bash # 运行所有测试并检测竞态条件 @@ -378,7 +424,7 @@ go test -race ./... go test -race -v ./internal/domain/... ``` -### 8.2 并发安全测试示例 +### 9.2 并发安全测试示例 ```go func TestConcurrentAccountAccess(t *testing.T) { @@ -400,16 +446,45 @@ func TestConcurrentAccountAccess(t *testing.T) { --- -## 9. 测试运行策略 +## 10. 性能回归测试 -### 9.1 本地开发 +### 10.1 执行时间监控 + +```go +//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 本地开发 ```bash -# 快速测试(跳过慢速测试) +# 快速测试(跳过慢速和集成测试) go test -short ./... # 完整测试(含集成测试) -go test -tags=integration ./... +go test -tags=integration, slow ./... # 竞态检测 go test -race ./... @@ -421,7 +496,7 @@ go test ./internal/domain/... go test -v -cover ./internal/domain/... ``` -### 9.2 CI/CD +### 11.2 CI/CD ```yaml # .github/workflows/test.yml @@ -445,12 +520,8 @@ jobs: - name: Run integration tests run: go test -tags=integration -race -coverprofile=coverage.out ./... - - name: Check Coverage - run: | - go test -cover ./... > coverage.txt - cat coverage.txt - # 检查关键模块覆盖率 - grep "middleware" coverage.txt + - name: Run slow tests + run: go test -tags=slow ./... - name: Upload coverage uses: codecov/codecov-action@v4 @@ -460,16 +531,11 @@ jobs: --- -## 10. 常见问题处理 +## 12. 常见问题处理 -### 10.1 测试依赖外部服务 +### 12.1 测试依赖外部服务 ```go -// ❌ 依赖真实存储 -func TestSettlementService(t *testing.T) { - repo, _ := NewPostgresRepository(db) -} - // ✅ 使用 Mock func TestSettlementService(t *testing.T) { store := newMockSettlementStore() @@ -477,40 +543,45 @@ func TestSettlementService(t *testing.T) { } ``` -### 10.2 时间相关测试 +### 12.2 时间相关测试 ```go // 使用依赖注入 type SettlementService struct { - store SettlementStore - clock Clock // 注入时间依赖 -} - -func (s *SettlementService) Withdraw(ctx context.Context, supplierID int64, req *WithdrawRequest) (*Settlement, error) { - now := s.clock.Now() // 使用注入的时间 + store SettlementStore + clock Clock // 注入时间依赖 } ``` -### 10.3 Flaky 测试处理 +### 12.3 Flaky 测试处理 ```go +// ❌ 错误 - 在测试中重试 func TestNetworkCall(t *testing.T) { - // 重试机制 - var lastErr error for i := 0; i < 3; i++ { if err := attempt(); err == nil { return } - lastErr = err - time.Sleep(10 * time.Millisecond) } - t.Fatalf("failed after retries: %v", lastErr) +} + +// ✅ 正确 - 标记为已知问题并使用超时 +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) } ``` --- -## 11. 测试检查清单 +## 13. 测试检查清单 新代码合并前: @@ -522,32 +593,42 @@ func TestNetworkCall(t *testing.T) { - [ ] 测试名称符合规范 - [ ] 表驱动测试覆盖边界情况 - [ ] 集成测试在 CI 中正常运行 -- [ ] **Middleware 模块覆盖率优先改进**(当前 52.7% → 目标 80%) +- [ ] 性能测试在慢速测试套件中 --- -## 12. 下一步行动计划 +## 14. 下一步行动计划 ### ✅ 已完成 -1. **Domain 模块覆盖率提升** (40.7% → 71.2%) ✅ -2. **Middleware 模块覆盖率提升** (52.7% → 80.4%) ✅ -3. **Audit handler 模块覆盖率提升** (75% → 79.6%) ✅ +1. Domain 模块覆盖率提升 (40.7% → 71.2%) +2. Middleware 模块覆盖率提升 (52.7% → 80.4%) +3. Audit handler 模块覆盖率提升 (75% → 79.6%) -### P1 - 高优先级 -1. Repository 模块覆盖率提升(1.3% → 30%) -2. settlement.go 方法覆盖(部分方法 0%) +### P1 - 创建测试工具包 -### P2 - 中优先级 -3. IAM handler/service 测试补充 -4. HTTP API handler 测试补充 -5. E2E 测试骨架 +1. **testutil/factory** - 测试数据工厂 +2. **testutil/mock** - 统一Mock库 +3. **testutil/assert** - 自定义断言 + +### P2 - 完善集成测试 + +1. Repository 模块集成测试骨架 +2. Settlement Store 集成测试 + +### P3 - 补充测试类型 + +1. E2E 测试骨架 +2. 性能回归测试 --- -## 13. 参考资料 +## 15. 参考资料 +- [Google Testing Blog](https://testing.googleblog.com/) +- [Atlassian Testing Guide](https://www.atlassian.com/continuous-delivery/software-testing) - [Go Testing](https://pkg.go.dev/testing) - [testify](https://github.com/stretchr/testify) +- [testcontainers-go](https://github.com/testcontainers/testcontainers-go) - [Go Race Detector](https://go.dev/blog/race-detector) - [Advanced Testing in Go](https://google.github.io/aip/214)