Files
lijiaoqiao/supply-api/docs/testing_strategy_v1.md

703 lines
16 KiB
Markdown
Raw Normal View History

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