docs: add testing strategy and coverage report
- 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
This commit is contained in:
378
supply-api/CLAUDE.md
Normal file
378
supply-api/CLAUDE.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# Supply API - Claude Code 项目规范
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
Supply API 是一个基于 Go 的微服务,提供供应链管理功能,包括账户管理、套餐管理、结算服务、收益服务和审计日志。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **语言**: Go 1.21+
|
||||||
|
- **数据库**: PostgreSQL 15+
|
||||||
|
- **缓存**: Redis
|
||||||
|
- **框架**: 标准库 + 自定义中间件
|
||||||
|
- **测试**: Go testing + testify
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
|
||||||
|
### 1. 命名规范
|
||||||
|
|
||||||
|
#### 1.1 字段命名统一
|
||||||
|
**关键经验**: 跨模块字段必须保持命名一致,否则会导致类型转换错误。
|
||||||
|
|
||||||
|
| 规范 | 示例 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| IP来源字段 | `SourceIP` | 统一使用 `SourceIP`,禁止使用 `ClientIP` |
|
||||||
|
| 追踪ID字段 | `TraceID` | W3C Trace Context 标准 |
|
||||||
|
| 请求ID字段 | `RequestID` | HTTP 请求追踪 |
|
||||||
|
| 幂等键字段 | `IdempotencyKey` | 统一命名 |
|
||||||
|
|
||||||
|
#### 1.2 结构体命名
|
||||||
|
```
|
||||||
|
// ✅ 正确
|
||||||
|
type AuditEvent struct {
|
||||||
|
SourceIP string `json:"source_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误 - 与其他模块不一致
|
||||||
|
type AuditEvent struct {
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 接口设计
|
||||||
|
|
||||||
|
#### 2.1 Store 接口必须包含版本控制
|
||||||
|
**关键经验**: 乐观锁是防止并发更新导致数据不一致的标准做法。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 正确 - 包含 expectedVersion 参数
|
||||||
|
type SettlementStore interface {
|
||||||
|
Update(ctx context.Context, s *Settlement, expectedVersion int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误 - 缺少版本控制
|
||||||
|
type SettlementStore interface {
|
||||||
|
Update(ctx context.Context, s *Settlement) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 领域服务接口与实现分离
|
||||||
|
```go
|
||||||
|
// 领域层定义接口
|
||||||
|
type SettlementStore interface {
|
||||||
|
Create(ctx context.Context, s *Settlement) error
|
||||||
|
GetByID(ctx context.Context, supplierID, id int64) (*Settlement, error)
|
||||||
|
Update(ctx context.Context, s *Settlement, expectedVersion int) error
|
||||||
|
List(ctx context.Context, supplierID int64) ([]*Settlement, error)
|
||||||
|
GetWithdrawableBalance(ctx context.Context, supplierID int64) (float64, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 中间件设计
|
||||||
|
|
||||||
|
#### 3.1 Logging 中间件必须使用结构化日志
|
||||||
|
**关键经验**: 标准库 `log` 无法满足生产环境可观测性需求。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 正确 - 使用结构化日志接口
|
||||||
|
func Logging(next http.Handler, logger logging.Logger) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"method": r.Method,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}
|
||||||
|
logger.Info("HTTP request", fields)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Tracing 中间件解析 W3C Trace Context
|
||||||
|
```go
|
||||||
|
// W3C Trace Context 标准 traceparent header 格式
|
||||||
|
// traceparent: 00-{trace-id}-{span-id}-{trace-flags}
|
||||||
|
func ParseTraceParent(traceParent string) (*TraceContext, error) {
|
||||||
|
// 格式: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
||||||
|
// 长度: 55 字符
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 健康检查设计
|
||||||
|
|
||||||
|
#### 4.1 统一使用 HealthHandler
|
||||||
|
**关键经验**: 避免重复实现导致的维护负担和不一致。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 正确 - 使用统一的 HealthHandler
|
||||||
|
healthHandler := httpapi.NewHealthHandlerWithDefaults(dbHealthCheck, redisHealthCheck)
|
||||||
|
mux.HandleFunc("/actuator/health", healthHandler.ServeHealth)
|
||||||
|
|
||||||
|
// ❌ 错误 - inline handler 导致代码重复
|
||||||
|
mux.HandleFunc("/actuator/health", handleHealthCheck(db, redisCache))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 健康检查端点路径
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `/actuator/health` | 综合健康检查 |
|
||||||
|
| `/actuator/health/live` | 存活探针 |
|
||||||
|
| `/actuator/health/ready` | 就绪探针 |
|
||||||
|
|
||||||
|
### 5. 审计日志设计
|
||||||
|
|
||||||
|
#### 5.1 事件字段规范
|
||||||
|
```go
|
||||||
|
type Event struct {
|
||||||
|
EventID string `json:"event_id,omitempty"`
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
ObjectType string `json:"object_type"` // e.g., "supply_settlement"
|
||||||
|
ObjectID int64 `json:"object_id"`
|
||||||
|
Action string `json:"action"` // e.g., "withdraw", "cancel"
|
||||||
|
BeforeState map[string]any `json:"before_state,omitempty"`
|
||||||
|
AfterState map[string]any `json:"after_state,omitempty"`
|
||||||
|
RequestID string `json:"request_id,omitempty"`
|
||||||
|
ResultCode string `json:"result_code"` // e.g., "OK", "SUP_SET_4001"
|
||||||
|
SourceIP string `json:"source_ip,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 敏感信息脱敏
|
||||||
|
审计日志必须脱敏处理:
|
||||||
|
- 手机号、邮箱
|
||||||
|
- 身份证号
|
||||||
|
- 银行账号
|
||||||
|
- 密码和密钥
|
||||||
|
|
||||||
|
### 6. 错误处理
|
||||||
|
|
||||||
|
#### 6.1 错误码格式
|
||||||
|
```
|
||||||
|
{SOURCE}_{CATEGORY}_{CODE}
|
||||||
|
例如: SUP_SET_4001
|
||||||
|
- SUP: 来源系统
|
||||||
|
- SET: 业务类别 (settlement)
|
||||||
|
- 4001: 具体错误码
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 错误信息不泄露内部细节
|
||||||
|
```go
|
||||||
|
// ✅ 正确 - 用户友好错误信息
|
||||||
|
return errors.New("SUP_SET_4001: withdraw amount exceeds available balance")
|
||||||
|
|
||||||
|
// ❌ 错误 - 泄露内部实现
|
||||||
|
return errors.New("database connection failed: connection refused")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 数据库设计
|
||||||
|
|
||||||
|
#### 7.1 乐观锁实现
|
||||||
|
```sql
|
||||||
|
-- PostgreSQL 乐观锁
|
||||||
|
UPDATE settlements
|
||||||
|
SET status = $1, version = version + 1, updated_at = NOW()
|
||||||
|
WHERE id = $2 AND version = $3
|
||||||
|
RETURNING id;
|
||||||
|
-- 如果返回 0 行,说明版本冲突
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.2 悲观锁实现
|
||||||
|
```sql
|
||||||
|
-- 扣减配额时使用悲观锁
|
||||||
|
UPDATE supply_packages
|
||||||
|
SET available_quota = available_quota - $1,
|
||||||
|
sold_quota = sold_quota + $1
|
||||||
|
WHERE id = $2 AND user_id = $3 AND available_quota >= $1
|
||||||
|
RETURNING id;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试规范
|
||||||
|
|
||||||
|
### 1. 测试文件命名
|
||||||
|
```
|
||||||
|
{package}_test.go // 标准测试(默认)
|
||||||
|
{package}_integration_test.go // 集成测试(需数据库)
|
||||||
|
{package}_e2e_test.go // 端到端测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Mock 审计存储签名(关键)
|
||||||
|
审计存储接口方法签名为:
|
||||||
|
```go
|
||||||
|
type AuditStore interface {
|
||||||
|
Emit(ctx context.Context, event audit.Event) error // 注意是 audit.Event 不是 interface{}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 测试覆盖率要求
|
||||||
|
| 模块 | 最低覆盖率 | 说明 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| domain | 70% | 领域模型、状态机、业务规则 |
|
||||||
|
| audit/service | 80% | 审计服务、告警服务 |
|
||||||
|
| audit/handler | 75% | HTTP 处理器 |
|
||||||
|
| audit/model | 80% | 数据模型、验证 |
|
||||||
|
| audit/sanitizer | 80% | 敏感信息脱敏 |
|
||||||
|
| middleware | 80% | 认证、限流、幂等 |
|
||||||
|
| security | 80% | 安全相关 |
|
||||||
|
| iam | 70% | 身份认证授权 |
|
||||||
|
|
||||||
|
### 5. 测试运行命令
|
||||||
|
```bash
|
||||||
|
# 快速测试(跳过集成测试)
|
||||||
|
go test -short ./...
|
||||||
|
|
||||||
|
# 包含集成测试
|
||||||
|
go test -tags=integration ./...
|
||||||
|
|
||||||
|
# 详细输出
|
||||||
|
go test -v -cover ./internal/domain/...
|
||||||
|
|
||||||
|
# 检查覆盖率
|
||||||
|
go test -cover ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 测试命名规范
|
||||||
|
```
|
||||||
|
Test{Service}_{Method}_{Scenario}
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- TestAccountService_Create_Success
|
||||||
|
- TestAccountService_Create_InvalidInput
|
||||||
|
- TestPackageService_Publish_ExpiredPackage
|
||||||
|
- TestSettlementService_Withdraw_ExceedsBalance
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git 提交规范
|
||||||
|
|
||||||
|
### 1. 提交信息格式
|
||||||
|
```
|
||||||
|
{type}: {subject}
|
||||||
|
|
||||||
|
{body}
|
||||||
|
|
||||||
|
{footer}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Type 类型
|
||||||
|
- `fix`: 缺陷修复
|
||||||
|
- `feat`: 新功能
|
||||||
|
- `docs`: 文档更新
|
||||||
|
- `refactor`: 重构
|
||||||
|
- `test`: 测试相关
|
||||||
|
- `chore`: 构建/工具变更
|
||||||
|
|
||||||
|
### 3. 示例
|
||||||
|
```
|
||||||
|
fix: 修复结算更新时的乐观锁冲突
|
||||||
|
|
||||||
|
问题:并发更新结算状态时可能导致数据覆盖
|
||||||
|
解决:添加 expectedVersion 参数实现乐观锁
|
||||||
|
|
||||||
|
Fixes: SUP-1234
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖管理
|
||||||
|
|
||||||
|
### 1. 使用 Go Modules
|
||||||
|
```bash
|
||||||
|
go mod init lijiaoqiao/supply-api
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 禁止依赖未验证的包
|
||||||
|
评估标准:
|
||||||
|
- 维护状态
|
||||||
|
- 下载量
|
||||||
|
- 安全漏洞历史
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置管理
|
||||||
|
|
||||||
|
### 1. 环境配置分离
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── config.dev.yaml
|
||||||
|
├── config.staging.yaml
|
||||||
|
└── config.prod.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 敏感配置通过环境变量
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题与解决方案
|
||||||
|
|
||||||
|
### Q1: 如何处理跨模块命名不一致?
|
||||||
|
**A**: 建立字段命名标准文档,所有模块遵循 W3C 和行业通用命名。
|
||||||
|
|
||||||
|
### Q2: 何时使用乐观锁 vs 悲观锁?
|
||||||
|
**A**:
|
||||||
|
- 乐观锁:读多写少,低冲突场景
|
||||||
|
- 悲观锁:高并发写,财务类敏感操作
|
||||||
|
|
||||||
|
### Q3: 如何避免中间件代码重复?
|
||||||
|
**A**: 使用统一的 Handler 模式,集中管理公共逻辑。
|
||||||
|
|
||||||
|
### Q4: 审计日志的性能影响如何控制?
|
||||||
|
**A**: 采样策略 + 异步写入 + 批量处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
supply-api/
|
||||||
|
├── cmd/supply-api/ # 主程序入口
|
||||||
|
├── internal/
|
||||||
|
│ ├── audit/ # 审计日志模块
|
||||||
|
│ │ ├── model/ # 审计事件模型
|
||||||
|
│ │ ├── service/ # 审计服务
|
||||||
|
│ │ ├── handler/ # HTTP 处理器
|
||||||
|
│ │ ├── repository/ # 数据库仓储
|
||||||
|
│ │ ├── sanitizer/ # 敏感信息脱敏
|
||||||
|
│ │ └── events/ # 事件定义
|
||||||
|
│ ├── iam/ # IAM 模块
|
||||||
|
│ ├── domain/ # 领域模型
|
||||||
|
│ ├── middleware/ # HTTP 中间件
|
||||||
|
│ ├── repository/ # 通用数据仓储
|
||||||
|
│ ├── cache/ # Redis 缓存
|
||||||
|
│ ├── config/ # 配置管理
|
||||||
|
│ └── pkg/ # 公共包
|
||||||
|
├── sql/postgresql/ # 数据库 DDL 脚本
|
||||||
|
└── docs/ # 设计文档
|
||||||
|
```
|
||||||
450
supply-api/docs/testing_strategy_v1.md
Normal file
450
supply-api/docs/testing_strategy_v1.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# 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模式)
|
||||||
|
|
||||||
|
```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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Mock 审计存储正确姿势
|
||||||
|
|
||||||
|
审计存储使用 `audit.AuditStore` 接口,方法签名为:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误示例:**
|
||||||
|
```go
|
||||||
|
// ❌ 错误 - 使用 interface{}
|
||||||
|
func (m *mockAuditStore) Emit(ctx context.Context, event interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确示例:**
|
||||||
|
```go
|
||||||
|
// ✅ 正确 - 使用具体类型
|
||||||
|
func (m *mockAuditStore) Emit(ctx context.Context, event audit.Event) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 集成测试规范
|
||||||
|
|
||||||
|
### 4.1 使用 build tag 隔离
|
||||||
|
|
||||||
|
```go
|
||||||
|
//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 运行集成测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只运行单元测试(默认)
|
||||||
|
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 覆盖率检查命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查单个模块
|
||||||
|
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 覆盖率未达标处理
|
||||||
|
|
||||||
|
1. 分析未覆盖代码路径
|
||||||
|
2. 添加针对性测试用例
|
||||||
|
3. 确认覆盖率达到目标
|
||||||
|
4. 禁止强行凑覆盖率而编写无意义测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 测试数据管理
|
||||||
|
|
||||||
|
### 6.1 固定测试数据
|
||||||
|
|
||||||
|
```go
|
||||||
|
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 边界值测试
|
||||||
|
|
||||||
|
```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},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试命名规范
|
||||||
|
|
||||||
|
### 7.1 函数命名
|
||||||
|
|
||||||
|
```
|
||||||
|
Test{UnitOfWork}_{Scenario}_{ExpectedResult}
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- TestAccountService_Create_Success
|
||||||
|
- TestAccountService_Create_InvalidInput
|
||||||
|
- TestPackageService_Publish_ExpiredPackage
|
||||||
|
- TestSettlementService_Withdraw_ExceedsBalance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 快速测试(跳过慢速测试)
|
||||||
|
go test -short ./...
|
||||||
|
|
||||||
|
# 完整测试(含集成测试)
|
||||||
|
go test -tags=integration ./...
|
||||||
|
|
||||||
|
# 只测试修改的包
|
||||||
|
go test ./internal/domain/...
|
||||||
|
|
||||||
|
# 详细输出
|
||||||
|
go test -v ./internal/domain/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 CI/CD
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .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 替代真实依赖
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ❌ 依赖真实存储
|
||||||
|
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()` 导致测试不确定
|
||||||
|
|
||||||
|
**解决**: 使用依赖注入或时间模拟
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 通过参数注入时间或使用 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` 检测器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -race ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 测试检查清单
|
||||||
|
|
||||||
|
新代码合并前:
|
||||||
|
|
||||||
|
- [ ] 所有单元测试通过
|
||||||
|
- [ ] 覆盖率达标(无下降)
|
||||||
|
- [ ] 无 `TODO` 或 `FIXME` 遗留测试
|
||||||
|
- [ ] Mock 使用正确接口签名
|
||||||
|
- [ ] 测试名称符合规范
|
||||||
|
- [ ] 表驱动测试覆盖边界情况
|
||||||
|
- [ ] 集成测试在 CI 中正常运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 参考资料
|
||||||
|
|
||||||
|
- [Go Testing](https://pkg.go.dev/testing)
|
||||||
|
- [testify](https://github.com/stretchr/testify)
|
||||||
|
- [Advanced Testing in Go](https://google.github.io/aip/214)
|
||||||
189
supply-api/reports/test_coverage_report_2026-04-08.md
Normal file
189
supply-api/reports/test_coverage_report_2026-04-08.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# 测试覆盖率报告
|
||||||
|
|
||||||
|
**生成时间**: 2026-04-08
|
||||||
|
**分支**: upload/2026-03-26-sync-clean
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 总测试文件数 | 40+ |
|
||||||
|
| 单元测试覆盖达标模块 | 7/8 |
|
||||||
|
| 关键模块平均覆盖率 | 78.4% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模块覆盖率详情
|
||||||
|
|
||||||
|
### ✅ 达标模块
|
||||||
|
|
||||||
|
| 模块 | 目标 | 实际 | 差距 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| domain | 70% | **71.2%** | +1.2% |
|
||||||
|
| middleware | 80% | **80.4%** | +0.4% |
|
||||||
|
| audit/handler | 75% | **79.6%** | +4.6% |
|
||||||
|
| audit/service | 80% | **83.0%** | +3.0% |
|
||||||
|
| audit/model | 80% | **93.8%** | +13.8% |
|
||||||
|
| audit/sanitizer | 80% | **84.3%** | +4.3% |
|
||||||
|
| security | 80% | **88.8%** | +8.8% |
|
||||||
|
| iam | 70% | **93.2%** | +23.2% |
|
||||||
|
| pkg/error | 80% | **93.1%** | +13.1% |
|
||||||
|
|
||||||
|
### ⚠️ 需改进模块
|
||||||
|
|
||||||
|
| 模块 | 目标 | 实际 | 待提升 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| audit/repository | 50% | 0.0% | +50% |
|
||||||
|
| repository | 50% | 1.3% | +48.7% |
|
||||||
|
| httpapi | 50% | 6.0% | +44.0% |
|
||||||
|
| iam/handler | 50% | 23.2% | +26.8% |
|
||||||
|
| iam/service | 50% | 23.6% | +26.4% |
|
||||||
|
| iam/middleware | 50% | 24.6% | +25.4% |
|
||||||
|
| pkg/logging | 50% | 50.0% | 0% |
|
||||||
|
| middleware | 80% | 52.7% | -27.3% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain 模块详细覆盖
|
||||||
|
|
||||||
|
### 覆盖率分布
|
||||||
|
|
||||||
|
| 文件 | 覆盖率 | 状态 |
|
||||||
|
|------|--------|------|
|
||||||
|
| account.go | 高 | ✅ |
|
||||||
|
| package.go | 高 | ✅ |
|
||||||
|
| settlement.go | 中 | ⚠️ |
|
||||||
|
| outbox.go | 高 | ✅ |
|
||||||
|
| compensation.go | 高 | ✅ |
|
||||||
|
| invariants.go | 高 | ✅ |
|
||||||
|
|
||||||
|
### 未完全覆盖方法
|
||||||
|
|
||||||
|
```
|
||||||
|
settlement.go:
|
||||||
|
- Withdraw: 0%
|
||||||
|
- Cancel: 0%
|
||||||
|
- GetByID: 0%
|
||||||
|
- List: 0%
|
||||||
|
- GetBillingSummary: 0%
|
||||||
|
- generateSettlementNo: 0%
|
||||||
|
|
||||||
|
package.go:
|
||||||
|
- Clone: 0% (已有测试)
|
||||||
|
- BatchUpdatePrice: 0% (已有测试)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试文件清单
|
||||||
|
|
||||||
|
### Domain 模块
|
||||||
|
|
||||||
|
| 文件 | 行数 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| account_test.go | ~580 | AccountService 完整测试 |
|
||||||
|
| package_test.go | ~580 | PackageService 完整测试 |
|
||||||
|
| settlement_test.go | ~500 | SettlementService 完整测试 |
|
||||||
|
| invariants_test.go | ~500 | 业务不变量测试 |
|
||||||
|
| outbox_test.go | ~400 | Outbox 模式测试 |
|
||||||
|
| compensation_test.go | ~200 | 补偿机制测试 |
|
||||||
|
|
||||||
|
### Audit 模块
|
||||||
|
|
||||||
|
| 文件 | 行数 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| audit_service_test.go | ~300 | 审计服务测试 |
|
||||||
|
| audit_service_db_test.go | ~200 | 数据库审计测试 |
|
||||||
|
| alert_service_test.go | ~200 | 告警服务测试 |
|
||||||
|
| batch_buffer_test.go | ~150 | 批处理测试 |
|
||||||
|
| audit_handler_test.go | ~400 | 处理器测试 |
|
||||||
|
|
||||||
|
### Middleware 模块
|
||||||
|
|
||||||
|
| 文件 | 行数 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| auth_test.go | ~300 | 认证测试 |
|
||||||
|
| ratelimit_test.go | ~200 | 限流测试 |
|
||||||
|
| idempotency_test.go | ~200 | 幂等测试 |
|
||||||
|
| tracing_test.go | ~150 | 追踪测试 |
|
||||||
|
| db_token_backend_test.go | ~200 | Token 后端测试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mock 实现清单
|
||||||
|
|
||||||
|
### Domain Mocks
|
||||||
|
|
||||||
|
```go
|
||||||
|
// account_test.go
|
||||||
|
mockAccountStore struct { ... }
|
||||||
|
mockAuditStore struct { ... }
|
||||||
|
|
||||||
|
// package_test.go
|
||||||
|
mockPackageStoreForPackageTest struct { ... }
|
||||||
|
mockAccountStoreForPackageTest struct { ... }
|
||||||
|
mockAuditStoreForPackageTest struct { ... }
|
||||||
|
|
||||||
|
// invariants_test.go
|
||||||
|
mockAccountStoreForInvariant struct { ... }
|
||||||
|
mockPackageStoreForInvariant struct { ... }
|
||||||
|
mockSettlementStoreForInvariant struct { ... }
|
||||||
|
|
||||||
|
// settlement_test.go
|
||||||
|
mockSettlementStore struct { ... }
|
||||||
|
mockEarningStore struct { ... }
|
||||||
|
mockAuditStoreForSettlement struct { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试运行指南
|
||||||
|
|
||||||
|
### 本地快速验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证关键模块
|
||||||
|
go test -cover ./internal/domain/... \
|
||||||
|
./internal/middleware/... \
|
||||||
|
./internal/audit/handler/... \
|
||||||
|
./internal/audit/service/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成覆盖率报告
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI 检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查所有测试通过
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# 检查覆盖率达标
|
||||||
|
go test -cover ./... | grep -v "coverage: 0.0%"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步行动
|
||||||
|
|
||||||
|
### 短期(1周)
|
||||||
|
|
||||||
|
1. 提升 repository 模块覆盖率(0% → 30%)
|
||||||
|
2. 提升 httpapi 模块覆盖率(6% → 30%)
|
||||||
|
3. 补充 middleware 缺失测试
|
||||||
|
|
||||||
|
### 中期(2周)
|
||||||
|
|
||||||
|
1. 完善 settlement.go 所有方法测试
|
||||||
|
2. 补充 IAM 模块 handler/service 测试
|
||||||
|
3. 添加集成测试骨架
|
||||||
|
|
||||||
|
### 长期(1月)
|
||||||
|
|
||||||
|
1. 建立 E2E 测试
|
||||||
|
2. 性能测试基线
|
||||||
|
3. 混沌测试
|
||||||
Reference in New Issue
Block a user