Files
lijiaoqiao/supply-api/docs/testing_strategy_v1.md
Your Name 85dac3ad44 fix: 修复 TimeoutMiddleware 并发问题并更新测试文档
问题修复:
- 修复 TimeoutMiddleware 死锁问题(嵌套锁调用)
- 修复竞态条件(responseSent 标志确保只发送一次响应)
- 基准测试超时从 5ms 改为 100ms 避免 race 检测不稳定

文档更新:
- 添加中间件并发测试要点(testing_strategy_v1.md)
- 添加 TimeoutMiddleware 并发安全经验(project_experience_summary.md)
- 更新测试覆盖率报告
- 新建项目状态报告
2026-04-08 18:20:40 +08:00

703 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
//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 测试数据工厂(新增)
```go
// 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 固定测试数据
```go
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 边界值测试
```go
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模式)
```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
// +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
//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
# 只运行单元测试(默认)
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 覆盖率检查命令
```bash
# ✅ 推荐:单独验证关键模块(显示真实覆盖率)
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 覆盖率未达标处理
1. 分析未覆盖代码路径
2. 添加针对性测试用例
3. 确认覆盖率达到目标
4. **禁止强行凑覆盖率而编写无意义测试**
---
## 8. 测试命名规范
### 8.1 函数命名
```
Test{Service}_{Method}_{Scenario}
示例:
- TestAccountService_Create_Success
- TestAccountService_Create_InvalidInput
- TestPackageService_Publish_ExpiredPackage
- TestSettlementService_Withdraw_ExceedsBalance
```
### 8.2 子测试命名
```go
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 检测
```bash
# 运行所有测试并检测竞态条件
go test -race ./...
# 详细输出
go test -race -v ./internal/domain/...
```
### 9.2 并发安全测试示例
```go
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 正确测试模式**
```go
// ✅ 正确:超时设置足够长(>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()
// 现在可以安全检查结果
}
```
**⚠️ 常见错误**
```go
// ❌ 错误:超时设置过短(<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 模式下通过:
```bash
# 必须验证
go test -race ./internal/middleware/...
# 基准测试也需要 race 检测
go test -race -bench=. ./internal/benchmark/...
```
---
## 10. 性能回归测试
### 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, slow ./...
# 竞态检测
go test -race ./...
# 只测试修改的包
go test ./internal/domain/...
# 详细输出
go test -v -cover ./internal/domain/...
```
### 11.2 CI/CD
```yaml
# .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 测试依赖外部服务
```go
// ✅ 使用 Mock
func TestSettlementService(t *testing.T) {
store := newMockSettlementStore()
svc := NewSettlementService(store, nil, nil)
}
```
### 12.2 时间相关测试
```go
// 使用依赖注入
type SettlementService struct {
store SettlementStore
clock Clock // 注入时间依赖
}
```
### 12.3 Flaky 测试处理
```go
// ❌ 错误 - 在测试中重试
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. 下一步行动计划
### ✅ 已完成
1. Domain 模块覆盖率提升 (40.7% → 71.2%)
2. Middleware 模块覆盖率提升 (52.7% → 80.4%)
3. Audit handler 模块覆盖率提升 (75% → 79.6%)
### P1 - 创建测试工具包
1. **testutil/factory** - 测试数据工厂
2. **testutil/mock** - 统一Mock库
3. **testutil/assert** - 自定义断言
### P2 - 完善集成测试
1. Repository 模块集成测试骨架
2. Settlement Store 集成测试
### P3 - 补充测试类型
1. E2E 测试骨架
2. 性能回归测试
---
## 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)