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%
This commit is contained in:
Your Name
2026-04-08 10:23:13 +08:00
parent 698759b665
commit 4349666ccb

View File

@@ -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)