- docs/testing_strategy_v1.md: comprehensive testing strategy - Test pyramid definition (Unit/Integration/E2E) - File naming conventions - Mock interface guidelines - Coverage requirements - Test execution commands - Common issues solutions - reports/test_coverage_report_2026-04-08.md: current coverage status - Module-by-module breakdown - Coverage达标情况 - Mock implementations inventory - Next action items - CLAUDE.md: update test specifications - Add audit store mock signature - Detailed coverage targets per module - Test naming conventions
10 KiB
10 KiB
Supply API 测试方案 v1.0
1. 概述
本文档定义了 Supply API 项目的系统性测试策略,确保代码质量、可维护性和快速迭代能力。
1.1 测试金字塔
┌─────────────┐
│ E2E │ ← 少量关键路径验证
┌─────────────┐
│ Integration│ ← API、DB、消息队列集成
┌───────────────┐
│ Unit │ ← 大量快速反馈
┌─────────────────┐
│ Component │ ← 单组件内部逻辑
┌───────────────────┐
1.2 测试类型定义
| 类型 | 目标 | 工具 | 速度 |
|---|---|---|---|
| 单元测试 | 业务逻辑、领域模型 | Go testing + testify | < 1ms |
| 集成测试 | Store、Repository、DB | go:build integration | < 100ms |
| E2E测试 | 关键业务流程 | Playwright | < 1s |
2. 测试组织结构
2.1 文件命名规范
{package}_test.go // 单元测试(默认)
{package}_integration_test.go // 集成测试(需数据库)
{package}_e2e_test.go // 端到端测试
2.2 测试包结构
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)
}
错误示例:
// ❌ 错误 - 使用 interface{}
func (m *mockAuditStore) Emit(ctx context.Context, event interface{}) error {
return nil
}
正确示例:
// ✅ 正确 - 使用具体类型
func (m *mockAuditStore) Emit(ctx context.Context, event audit.Event) error {
return nil
}
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 使用 build tag 隔离
//go:build integration
// +build integration
package repository_test
import (
"testing"
"github.com/stretchr/testify/assert"
"lijiaoqiao/supply-api/internal/repository"
)
// IntegrationTestSettlementRepository 需要真实的 PostgreSQL
func TestIntegrationSettlementRepository(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// ...
}
4.2 运行集成测试
# 只运行单元测试(默认)
go test ./...
# 包含集成测试
go test -tags=integration ./...
# 排除集成测试
go test -short ./...
5. 覆盖率要求
5.1 模块覆盖率目标
| 模块 | 最低覆盖率 | 当前覆盖率 | 状态 |
|---|---|---|---|
| domain | 70% | 71.2% | ✅ |
| middleware | 80% | 80.4% | ✅ |
| audit/service | 80% | 83.0% | ✅ |
| audit/handler | 75% | 79.6% | ✅ |
| audit/model | 80% | 93.8% | ✅ |
| audit/sanitizer | 80% | 84.3% | ✅ |
| security | 80% | 88.8% | ✅ |
| iam | 70% | 93.2% | ✅ |
5.2 覆盖率检查命令
# 检查单个模块
go test -cover ./internal/domain/...
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# 检查覆盖率达标情况
go test -cover ./... 2>&1 | grep -E "(coverage|FAIL)"
5.3 覆盖率未达标处理
- 分析未覆盖代码路径
- 添加针对性测试用例
- 确认覆盖率达到目标
- 禁止强行凑覆盖率而编写无意义测试
6. 测试数据管理
6.1 固定测试数据
func TestAccountService_Create(t *testing.T) {
store := newMockAccountStore()
req := &CreateAccountRequest{
SupplierID: 1001, // 固定供应商ID
Provider: ProviderOpenAI, // 固定提供商
AccountType: AccountTypeAPIKey,
Credential: "sk-test-key", // 测试用密钥
RiskAck: true,
}
account, err := store.Create(context.Background(), req)
// ...
}
6.2 边界值测试
tests := []struct {
name string
input float64
want bool
}{
{"zero", 0.0, true},
{"positive", 100.0, true},
{"negative", -1.0, false},
{"very large", 1e10, true},
}
7. 测试命名规范
7.1 函数命名
Test{UnitOfWork}_{Scenario}_{ExpectedResult}
示例:
- 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 本地开发
# 快速测试(跳过慢速测试)
go test -short ./...
# 完整测试(含集成测试)
go test -tags=integration ./...
# 只测试修改的包
go test ./internal/domain/...
# 详细输出
go test -v ./internal/domain/...
8.2 CI/CD
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run unit tests
run: go test -short -coverprofile=coverage.out ./...
- name: Run integration tests
run: go test -tags=integration -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
9. 常见问题处理
9.1 测试依赖外部服务
问题: 数据库、Redis、消息队列不可用
解决: 使用 Mock 替代真实依赖
// ❌ 依赖真实存储
func TestSettlementService(t *testing.T) {
repo, _ := NewPostgresRepository(db) // 需要真实DB
}
// ✅ 使用 Mock
func TestSettlementService(t *testing.T) {
store := newMockSettlementStore() // 无外部依赖
svc := NewSettlementService(store, nil, nil)
}
9.2 时间相关测试
问题: time.Now() 导致测试不确定
解决: 使用依赖注入或时间模拟
// 通过参数注入时间或使用 clock 接口
type SettlementService struct {
store SettlementStore
clock Clock // 注入时间依赖
}
func (s *SettlementService) Withdraw(ctx context.Context, supplierID int64, req *WithdrawRequest) (*Settlement, error) {
now := s.clock.Now()
// 使用 now 而非 time.Now()
}
9.3 并发测试
问题: 竞态条件难以复现
解决: 使用 race 检测器
go test -race ./...
10. 测试检查清单
新代码合并前:
- 所有单元测试通过
- 覆盖率达标(无下降)
- 无
TODO或FIXME遗留测试 - Mock 使用正确接口签名
- 测试名称符合规范
- 表驱动测试覆盖边界情况
- 集成测试在 CI 中正常运行