Files
lijiaoqiao/supply-api/docs/testing_strategy_v1.md
Your Name 698759b665 docs: refine testing strategy based on code review
Changes:
- docs/testing_strategy_v1.md v1.1:
  - Clarify test pyramid with build tags (unit/integration/e2e)
  - Add Middleware to priority module list
  - Add coverage run discrepancy warning (individual vs ./...)
  - Update action items to reflect completed work
  - Add race detector usage guidelines
  - Add test cleanup patterns

- reports/test_coverage_report_2026-04-08.md:
  - Clarify coverage run discrepancy (individual vs ./...)
  - Add dual columns: individual run vs combined run
  - Update status to  all key modules passing
  - Add week-over-week trending
  - Remove incorrect "emergency" status
2026-04-08 10:15:45 +08:00

13 KiB
Raw Blame History

Supply API 测试方案 v1.1

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%),需优先改进。


2. 测试组织结构

2.1 文件命名规范

{package}_test.go              # 单元测试(默认,无 build tag
{package}_integration_test.go   # 集成测试(需数据库)
{package}_e2e_test.go          # E2E 测试(需完整环境)

2.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 测试

2.3 测试包结构

internal/
├── domain/                    # 领域模型
│   ├── account.go            # 账号领域逻辑
│   ├── account_test.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
│
├── middleware/              # HTTP中间件
│   ├── auth.go
│   ├── auth_test.go        # 认证测试
│   ├── ratelimit.go
│   └── ratelimit_test.go   # 限流测试

3. 单元测试规范

3.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)
}

3.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
}

3.3 Mock 审计存储正确姿势

审计存储使用 audit.AuditStore 接口:

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")
}

3.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)
        })
    }
}

4. 测试数据管理

4.1 测试 Setup/Teardown

func TestAccountService(t *testing.T) {
    store := newMockAccountStore()

    t.Cleanup(func() {
        // 清理测试数据(如果需要)
    })

    // 测试逻辑...
}

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 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
}

5.2 运行命令

# 只运行单元测试(默认)
go test ./...

# 包含集成测试
go test -tags=integration ./...

# 排除集成测试
go test -short ./...

# 运行特定 tag
go test -tags=unit ./internal/domain/...

6. 覆盖率要求

6.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% -

⚠️ 关键问题: Middleware 模块覆盖率 52.7%,与目标差距 -27.3%,需优先改进。

6.2 覆盖率检查命令

重要: Go test 在运行 go test ./... 时会进行覆盖率聚合,可能导致某些模块显示的覆盖率低于单独运行时的值。

# ✅ 推荐:单独验证关键模块(显示真实覆盖率)
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 -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# 检查覆盖率达标情况(使用单独运行)
go test -cover ./internal/domain/... 2>&1 | grep "coverage"

6.3 覆盖率未达标处理

  1. 分析未覆盖代码路径
  2. 添加针对性测试用例
  3. 确认覆盖率达到目标
  4. 禁止强行凑覆盖率而编写无意义测试

7. 测试命名规范

7.1 函数命名

Test{Service}_{Method}_{Scenario}

示例:
- TestAccountService_Create_Success
- TestAccountService_Create_InvalidInput
- TestPackageService_Publish_ExpiredPackage
- TestSettlementService_Withdraw_ExceedsBalance

7.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,
        },
        {
            name:       "activate non-existent fails",
            setup:      func() *Account { return nil },
            supplierID: 9999,
            wantErr:    true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // ...
        })
    }
}

8. 并发与竞态测试

8.1 启用 Race 检测

# 运行所有测试并检测竞态条件
go test -race ./...

# 详细输出
go test -race -v ./internal/domain/...

8.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. 测试运行策略

9.1 本地开发

# 快速测试(跳过慢速测试)
go test -short ./...

# 完整测试(含集成测试)
go test -tags=integration ./...

# 竞态检测
go test -race ./...

# 只测试修改的包
go test ./internal/domain/...

# 详细输出
go test -v -cover ./internal/domain/...

9.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: Check Coverage
        run: |
          go test -cover ./... > coverage.txt
          cat coverage.txt
          # 检查关键模块覆盖率
          grep "middleware" coverage.txt

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.out

10. 常见问题处理

10.1 测试依赖外部服务

// ❌ 依赖真实存储
func TestSettlementService(t *testing.T) {
    repo, _ := NewPostgresRepository(db)
}

// ✅ 使用 Mock
func TestSettlementService(t *testing.T) {
    store := newMockSettlementStore()
    svc := NewSettlementService(store, nil, nil)
}

10.2 时间相关测试

// 使用依赖注入
type SettlementService struct {
    store  SettlementStore
    clock  Clock  // 注入时间依赖
}

func (s *SettlementService) Withdraw(ctx context.Context, supplierID int64, req *WithdrawRequest) (*Settlement, error) {
    now := s.clock.Now()  // 使用注入的时间
}

10.3 Flaky 测试处理

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

11. 测试检查清单

新代码合并前:

  • 所有单元测试通过 (go test ./...)
  • 覆盖率达标(无下降)
  • Race 检测通过 (go test -race ./...)
  • TODOFIXME 遗留测试
  • Mock 使用正确接口签名
  • 测试名称符合规范
  • 表驱动测试覆盖边界情况
  • 集成测试在 CI 中正常运行
  • Middleware 模块覆盖率优先改进(当前 52.7% → 目标 80%

12. 下一步行动计划

已完成

  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%

P2 - 中优先级

  1. IAM handler/service 测试补充
  2. HTTP API handler 测试补充
  3. E2E 测试骨架

13. 参考资料