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

16 KiB
Raw Permalink Blame 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: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 覆盖率未达标处理

  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 子测试命名

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 ./...)
  • TODOFIXME 遗留测试
  • 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. 参考资料