test(supply-api): add benchmark and test helper support

Add benchmark documentation and middleware benchmark coverage, fix the settlement benchmark mock to satisfy the current SettlementStore interface, and add reusable domain test helper packages. Verified with fresh go test runs for ./internal/testutil/... and go test -tags=slow -run '^$' ./internal/benchmark/... before commit.
This commit is contained in:
Your Name
2026-04-11 11:18:45 +08:00
parent 6a5730a261
commit ee569e7edb
7 changed files with 767 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# 性能回归测试
本目录包含 Supply API 的性能回归测试,确保关键路径的性能不会随着代码变更而退化。
## 测试文件
| 文件 | 说明 |
|------|------|
| `domain_bench_test.go` | 领域模型性能测试(账号、套餐、结算服务) |
| `middleware_bench_test.go` | 中间件性能测试(认证、限流、追踪、幂等) |
## 运行方式
```bash
# 运行所有性能测试(跳过快速测试)
go test -tags=slow ./internal/benchmark/...
# 运行特定基准测试
go test -tags=slow -run=XXX ./internal/benchmark/... -bench=BenchmarkAccountService_Create
# 查看内存分配
go test -tags=slow ./internal/benchmark/... -bench=BenchmarkAccountService_Create -benchmem
# 输出详细信息
go test -tags=slow ./internal/benchmark/... -bench=BenchmarkAccountService_Create -v
```
## 性能目标
| 模块 | 操作 | 目标 | 阈值 |
|------|------|------|------|
| AccountService | Create | < 1ms | 5ms |
| AccountService | Verify | < 5ms | 10ms |
| PackageService | CreateDraft | < 2ms | 10ms |
| PackageService | BatchUpdatePrice(50) | < 20ms | 50ms |
| SettlementService | Withdraw | < 5ms | 20ms |
| Middleware | Auth | < 0.5ms | 1ms |
| Middleware | RateLimit | < 0.2ms | 0.5ms |
| Middleware | Tracing | < 0.1ms | 0.5ms |
## CI/CD 集成
性能测试在 CI/CD 中作为门禁检查:
```yaml
# .github/workflows/performance.yml
- name: Run Performance Tests
run: |
go test -tags=slow -bench=. -benchmem ./internal/benchmark/... \
| tee performance_report.txt
```
## 添加新的性能测试
```go
//go:build slow
// +build slow
package benchmark
func BenchmarkYourOperation(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
// 准备测试数据
setup()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// 执行被测操作
}
}
```

View File

@@ -340,6 +340,14 @@ func (m *mockSettlementStoreForBenchmark) Create(ctx context.Context, s *domain.
return nil
}
func (m *mockSettlementStoreForBenchmark) CreateWithdrawTx(ctx context.Context, s *domain.Settlement) error {
return m.Create(ctx, s)
}
func (m *mockSettlementStoreForBenchmark) CreateInTx(ctx context.Context, s *domain.Settlement) error {
return m.Create(ctx, s)
}
func (m *mockSettlementStoreForBenchmark) GetByID(ctx context.Context, supplierID, id int64) (*domain.Settlement, error) {
if s, ok := m.settlements[id]; ok && s.SupplierID == supplierID {
return s, nil

View File

@@ -0,0 +1,115 @@
//go:build slow
// +build slow
package benchmark
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"lijiaoqiao/supply-api/internal/pkg/logging"
"lijiaoqiao/supply-api/internal/middleware"
)
// mockLogger 用于基准测试的 mock Logger
type mockLogger struct{}
func (m *mockLogger) Debug(msg string, fields ...map[string]interface{}) {}
func (m *mockLogger) Info(msg string, fields ...map[string]interface{}) {}
func (m *mockLogger) Warn(msg string, fields ...map[string]interface{}) {}
func (m *mockLogger) Error(msg string, fields ...map[string]interface{}) {}
func (m *mockLogger) Fatal(msg string, fields ...map[string]interface{}) {}
func (m *mockLogger) WithFields(fields map[string]interface{}) logging.Logger { return m }
// BenchmarkLoggingMiddleware 基准测试:日志中间件性能
func BenchmarkLoggingMiddleware(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
logger := &mockLogger{}
loggingHandler := middleware.Logging(handler, logger)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
loggingHandler.ServeHTTP(rec, req)
}
}
// BenchmarkTracingMiddleware 基准测试:追踪中间件性能
func BenchmarkTracingMiddleware(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
tracingHandler := middleware.TracingMiddleware(handler)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
rec := httptest.NewRecorder()
tracingHandler.ServeHTTP(rec, req)
}
}
// BenchmarkTimeoutMiddleware 基准测试:超时中间件性能
func BenchmarkTimeoutMiddleware(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// 使用足够长的超时时间100ms确保 handler 几乎总是正常完成
// 避免因超时设置过短导致的 race 条件
timeoutHandler := middleware.WithTimeoutMiddleware(handler, 100*time.Millisecond)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
timeoutHandler.ServeHTTP(rec, req)
}
}
// BenchmarkHTTPHandler_Empty 基准测试:空 Handler 性能(基线)
func BenchmarkHTTPHandler_Empty(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "OK")
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
}
}

View File

@@ -0,0 +1,93 @@
package assert
import (
"testing"
"github.com/stretchr/testify/assert"
"lijiaoqiao/supply-api/internal/domain"
)
// Account 相关的自定义断言
// AssertAccountEqual 断言两个账号相等(忽略指针差异)
func AssertAccountEqual(t *testing.T, expected, actual *domain.Account, msgAndArgs ...interface{}) {
assert.Equal(t, expected, actual, msgAndArgs...)
}
// AssertAccountStatus 断言账号状态
func AssertAccountStatus(t *testing.T, account *domain.Account, expectedStatus domain.AccountStatus, msgAndArgs ...interface{}) {
assert.Equal(t, expectedStatus, account.Status, msgAndArgs...)
}
// AssertAccountFields 断言账号字段
func AssertAccountFields(t *testing.T, account *domain.Account, supplierID int64, provider domain.Provider, accountType domain.AccountType, msgAndArgs ...interface{}) {
assert.Equal(t, supplierID, account.SupplierID, msgAndArgs...)
assert.Equal(t, provider, account.Provider, msgAndArgs...)
assert.Equal(t, accountType, account.AccountType, msgAndArgs...)
}
// Package 相关的自定义断言
// AssertPackageEqual 断言两个套餐相等
func AssertPackageEqual(t *testing.T, expected, actual *domain.Package, msgAndArgs ...interface{}) {
assert.Equal(t, expected, actual, msgAndArgs...)
}
// AssertPackageStatus 断言套餐状态
func AssertPackageStatus(t *testing.T, pkg *domain.Package, expectedStatus domain.PackageStatus, msgAndArgs ...interface{}) {
assert.Equal(t, expectedStatus, pkg.Status, msgAndArgs...)
}
// AssertPackageQuota 断言套餐配额
func AssertPackageQuota(t *testing.T, pkg *domain.Package, total, available, sold float64, msgAndArgs ...interface{}) {
assert.InDelta(t, total, pkg.TotalQuota, 0.001, msgAndArgs...)
assert.InDelta(t, available, pkg.AvailableQuota, 0.001, msgAndArgs...)
assert.InDelta(t, sold, pkg.SoldQuota, 0.001, msgAndArgs...)
}
// Settlement 相关的自定义断言
// AssertSettlementEqual 断言两个结算单相等
func AssertSettlementEqual(t *testing.T, expected, actual *domain.Settlement, msgAndArgs ...interface{}) {
assert.Equal(t, expected, actual, msgAndArgs...)
}
// AssertSettlementStatus 断言结算单状态
func AssertSettlementStatus(t *testing.T, s *domain.Settlement, expectedStatus domain.SettlementStatus, msgAndArgs ...interface{}) {
assert.Equal(t, expectedStatus, s.Status, msgAndArgs...)
}
// AssertSettlementAmount 断言结算单金额
func AssertSettlementAmount(t *testing.T, s *domain.Settlement, total, fee, net float64, msgAndArgs ...interface{}) {
assert.InDelta(t, total, s.TotalAmount, 0.001, msgAndArgs...)
assert.InDelta(t, fee, s.FeeAmount, 0.001, msgAndArgs...)
assert.InDelta(t, net, s.NetAmount, 0.001, msgAndArgs...)
}
// EarningRecord 相关的自定义断言
// AssertEarningRecordEqual 断言两条收益记录相等
func AssertEarningRecordEqual(t *testing.T, expected, actual *domain.EarningRecord, msgAndArgs ...interface{}) {
assert.Equal(t, expected, actual, msgAndArgs...)
}
// Error 相关的自定义断言
// AssertErrorCode 断言错误码包含特定字符串
func AssertErrorCode(t *testing.T, err error, code string, msgAndArgs ...interface{}) {
if err == nil {
assert.Fail(t, "Expected error but got nil", msgAndArgs...)
return
}
assert.Contains(t, err.Error(), code, msgAndArgs...)
}
// AssertErrorMessage 断言错误信息
func AssertErrorMessage(t *testing.T, err error, msg string, msgAndArgs ...interface{}) {
if err == nil {
assert.Fail(t, "Expected error but got nil", msgAndArgs...)
return
}
assert.Contains(t, err.Error(), msg, msgAndArgs...)
}

View File

@@ -0,0 +1,126 @@
package factory
import (
"lijiaoqiao/supply-api/internal/domain"
)
// AccountFactory 账号测试数据工厂
type AccountFactory struct {
supplierID int64
provider domain.Provider
accountType domain.AccountType
credential string
alias string
riskAck bool
}
// NewAccountFactory 创建默认账号工厂
func NewAccountFactory() *AccountFactory {
return &AccountFactory{
supplierID: 1001,
provider: domain.ProviderOpenAI,
accountType: domain.AccountTypeAPIKey,
credential: "sk-test-key-" + randomString(8),
alias: "test-account",
riskAck: true,
}
}
// WithSupplierID 设置供应商ID
func (f *AccountFactory) WithSupplierID(id int64) *AccountFactory {
f.supplierID = id
return f
}
// WithProvider 设置提供商
func (f *AccountFactory) WithProvider(p domain.Provider) *AccountFactory {
f.provider = p
return f
}
// WithAccountType 设置账号类型
func (f *AccountFactory) WithAccountType(t domain.AccountType) *AccountFactory {
f.accountType = t
return f
}
// WithCredential 设置凭证
func (f *AccountFactory) WithCredential(cred string) *AccountFactory {
f.credential = cred
return f
}
// WithAlias 设置别名
func (f *AccountFactory) WithAlias(alias string) *AccountFactory {
f.alias = alias
return f
}
// WithRiskAck 设置风险确认
func (f *AccountFactory) WithRiskAck(ack bool) *AccountFactory {
f.riskAck = ack
return f
}
// BuildRequest 构建创建账号请求
func (f *AccountFactory) BuildRequest() *domain.CreateAccountRequest {
return &domain.CreateAccountRequest{
SupplierID: f.supplierID,
Provider: f.provider,
AccountType: f.accountType,
Credential: f.credential,
Alias: f.alias,
RiskAck: f.riskAck,
}
}
// Build 构建账号对象
func (f *AccountFactory) Build() *domain.Account {
return &domain.Account{
ID: 1,
SupplierID: f.supplierID,
Provider: f.provider,
AccountType: f.accountType,
CredentialHash: f.credential,
KeyID: "key-" + randomString(8),
Alias: f.alias,
Status: domain.AccountStatusPending,
RiskLevel: "low",
TosCompliant: f.riskAck,
Version: 1,
}
}
// BuildActive 构建活跃状态的账号
func (f *AccountFactory) BuildActive() *domain.Account {
account := f.Build()
account.Status = domain.AccountStatusActive
return account
}
// BuildSuspended 构建暂停状态的账号
func (f *AccountFactory) BuildSuspended() *domain.Account {
account := f.Build()
account.Status = domain.AccountStatusSuspended
return account
}
// BuildInvalidCredential 构建无效凭证的工厂
func (f *AccountFactory) BuildInvalidCredential() *AccountFactory {
return f.WithCredential("")
}
// BuildWithoutRiskAck 构建未确认风险的工厂
func (f *AccountFactory) BuildWithoutRiskAck() *AccountFactory {
return f.WithRiskAck(false)
}
// randomString 生成随机字符串(简化版)
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[i%len(letters)]
}
return string(b)
}

View File

@@ -0,0 +1,175 @@
package factory
import (
"time"
"lijiaoqiao/supply-api/internal/domain"
)
// PackageFactory 套餐测试数据工厂
type PackageFactory struct {
supplierID int64
accountID int64
model string
totalQuota float64
availableQuota float64
soldQuota float64
reservedQuota float64
pricePer1MInput float64
pricePer1MOutput float64
validDays int
maxConcurrent int
rateLimitRPM int
status domain.PackageStatus
quotaUnit string
priceUnit string
currencyCode string
}
// NewPackageFactory 创建默认套餐工厂
func NewPackageFactory() *PackageFactory {
return &PackageFactory{
supplierID: 1001,
accountID: 1,
model: "gpt-4o-mini",
totalQuota: 1000000,
availableQuota: 1000000,
soldQuota: 0,
reservedQuota: 0,
pricePer1MInput: 0.5,
pricePer1MOutput: 1.5,
validDays: 30,
maxConcurrent: 10,
rateLimitRPM: 1000,
status: domain.PackageStatusDraft,
quotaUnit: "tokens",
priceUnit: "USD",
currencyCode: "USDT",
}
}
// WithSupplierID 设置供应商ID
func (f *PackageFactory) WithSupplierID(id int64) *PackageFactory {
f.supplierID = id
return f
}
// WithAccountID 设置账户ID
func (f *PackageFactory) WithAccountID(id int64) *PackageFactory {
f.accountID = id
return f
}
// WithModel 设置模型
func (f *PackageFactory) WithModel(model string) *PackageFactory {
f.model = model
return f
}
// WithQuota 设置配额
func (f *PackageFactory) WithQuota(total, available float64) *PackageFactory {
f.totalQuota = total
f.availableQuota = available
return f
}
// WithPrice 设置价格
func (f *PackageFactory) WithPrice(input, output float64) *PackageFactory {
f.pricePer1MInput = input
f.pricePer1MOutput = output
return f
}
// WithValidDays 设置有效期
func (f *PackageFactory) WithValidDays(days int) *PackageFactory {
f.validDays = days
return f
}
// WithStatus 设置状态
func (f *PackageFactory) WithStatus(status domain.PackageStatus) *PackageFactory {
f.status = status
return f
}
// BuildRequest 构建创建套餐草稿请求
func (f *PackageFactory) BuildRequest() *domain.CreatePackageDraftRequest {
return &domain.CreatePackageDraftRequest{
SupplierID: f.supplierID,
AccountID: f.accountID,
Model: f.model,
TotalQuota: f.totalQuota,
PricePer1MInput: f.pricePer1MInput,
PricePer1MOutput: f.pricePer1MOutput,
ValidDays: f.validDays,
MaxConcurrent: f.maxConcurrent,
RateLimitRPM: f.rateLimitRPM,
}
}
// Build 构建套餐对象
func (f *PackageFactory) Build() *domain.Package {
now := time.Now()
return &domain.Package{
ID: 1,
SupplierID: f.supplierID,
AccountID: f.accountID,
Model: f.model,
TotalQuota: f.totalQuota,
AvailableQuota: f.availableQuota,
SoldQuota: f.soldQuota,
ReservedQuota: f.reservedQuota,
PricePer1MInput: f.pricePer1MInput,
PricePer1MOutput: f.pricePer1MOutput,
ValidDays: f.validDays,
MaxConcurrent: f.maxConcurrent,
RateLimitRPM: f.rateLimitRPM,
Status: f.status,
TotalOrders: 0,
TotalRevenue: 0,
Rating: 0,
RatingCount: 0,
QuotaUnit: f.quotaUnit,
PriceUnit: f.priceUnit,
CurrencyCode: f.currencyCode,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
}
}
// BuildActive 构建活跃状态的套餐
func (f *PackageFactory) BuildActive() *domain.Package {
pkg := f.Build()
pkg.Status = domain.PackageStatusActive
return pkg
}
// BuildPaused 构建暂停状态的套餐
func (f *PackageFactory) BuildPaused() *domain.Package {
pkg := f.Build()
pkg.Status = domain.PackageStatusPaused
return pkg
}
// BuildSoldOut 构建售罄状态的套餐
func (f *PackageFactory) BuildSoldOut() *domain.Package {
pkg := f.Build()
pkg.Status = domain.PackageStatusSoldOut
pkg.AvailableQuota = 0
return pkg
}
// BuildExpired 构建过期状态的套餐
func (f *PackageFactory) BuildExpired() *domain.Package {
pkg := f.Build()
pkg.Status = domain.PackageStatusExpired
return pkg
}
// BuildWithID 构建带指定ID的套餐
func (f *PackageFactory) BuildWithID(id int64) *domain.Package {
pkg := f.Build()
pkg.ID = id
return pkg
}

View File

@@ -0,0 +1,174 @@
package factory
import (
"time"
"lijiaoqiao/supply-api/internal/domain"
)
// SettlementFactory 结算测试数据工厂
type SettlementFactory struct {
supplierID int64
settlementNo string
status domain.SettlementStatus
totalAmount float64
feeAmount float64
netAmount float64
paymentMethod domain.PaymentMethod
paymentAccount string
paymentTransactionID string
periodStart time.Time
periodEnd time.Time
totalOrders int
totalUsageRecords int
currencyCode string
amountUnit string
requestID string
idempotencyKey string
}
// NewSettlementFactory 创建默认结算工厂
func NewSettlementFactory() *SettlementFactory {
now := time.Now()
return &SettlementFactory{
supplierID: 1001,
settlementNo: "SET" + now.Format("20060102150405"),
status: domain.SettlementStatusPending,
totalAmount: 1000.00,
feeAmount: 10.00,
netAmount: 990.00,
paymentMethod: domain.PaymentMethodBank,
paymentAccount: "bank-1234567890",
periodStart: now.AddDate(0, 0, -30),
periodEnd: now,
totalOrders: 100,
totalUsageRecords: 1000,
currencyCode: "USDT",
amountUnit: "USD",
}
}
// WithSupplierID 设置供应商ID
func (f *SettlementFactory) WithSupplierID(id int64) *SettlementFactory {
f.supplierID = id
return f
}
// WithStatus 设置状态
func (f *SettlementFactory) WithStatus(status domain.SettlementStatus) *SettlementFactory {
f.status = status
return f
}
// WithAmount 设置金额
func (f *SettlementFactory) WithAmount(total, fee, net float64) *SettlementFactory {
f.totalAmount = total
f.feeAmount = fee
f.netAmount = net
return f
}
// WithPaymentMethod 设置支付方式
func (f *SettlementFactory) WithPaymentMethod(method domain.PaymentMethod, account string) *SettlementFactory {
f.paymentMethod = method
f.paymentAccount = account
return f
}
// WithPeriod 设置账期
func (f *SettlementFactory) WithPeriod(start, end time.Time) *SettlementFactory {
f.periodStart = start
f.periodEnd = end
return f
}
// WithIdempotencyKey 设置幂等键
func (f *SettlementFactory) WithIdempotencyKey(key string) *SettlementFactory {
f.idempotencyKey = key
return f
}
// Build 构建结算单对象
func (f *SettlementFactory) Build() *domain.Settlement {
now := time.Now()
return &domain.Settlement{
ID: 1,
SupplierID: f.supplierID,
SettlementNo: f.settlementNo,
Status: f.status,
TotalAmount: f.totalAmount,
FeeAmount: f.feeAmount,
NetAmount: f.netAmount,
PaymentMethod: f.paymentMethod,
PaymentAccount: f.paymentAccount,
PaymentTransactionID: f.paymentTransactionID,
PeriodStart: f.periodStart,
PeriodEnd: f.periodEnd,
TotalOrders: f.totalOrders,
TotalUsageRecords: f.totalUsageRecords,
CurrencyCode: f.currencyCode,
AmountUnit: f.amountUnit,
RequestID: f.requestID,
IdempotencyKey: f.idempotencyKey,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
}
}
// BuildPending 构建待处理状态的结算单
func (f *SettlementFactory) BuildPending() *domain.Settlement {
s := f.Build()
s.Status = domain.SettlementStatusPending
return s
}
// BuildProcessing 构建处理中的结算单
func (f *SettlementFactory) BuildProcessing() *domain.Settlement {
s := f.Build()
s.Status = domain.SettlementStatusProcessing
return s
}
// BuildCompleted 构建已完成状态的结算单
func (f *SettlementFactory) BuildCompleted() *domain.Settlement {
s := f.Build()
s.Status = domain.SettlementStatusCompleted
now := time.Now()
s.PaidAt = &now
return s
}
// BuildFailed 构建失败状态的结算单
func (f *SettlementFactory) BuildFailed() *domain.Settlement {
s := f.Build()
s.Status = domain.SettlementStatusFailed
return s
}
// BuildWithID 构建带指定ID的结算单
func (f *SettlementFactory) BuildWithID(id int64) *domain.Settlement {
s := f.Build()
s.ID = id
return s
}
// WithdrawRequest 构建提现请求
func (f *SettlementFactory) WithdrawRequest() *domain.WithdrawRequest {
return &domain.WithdrawRequest{
Amount: f.totalAmount,
PaymentMethod: f.paymentMethod,
PaymentAccount: f.paymentAccount,
SMSCode: "123456",
}
}
// WithdrawRequestWithCode 构建带指定短信验证码的提现请求
func (f *SettlementFactory) WithdrawRequestWithCode(smsCode string) *domain.WithdrawRequest {
return &domain.WithdrawRequest{
Amount: f.totalAmount,
PaymentMethod: f.paymentMethod,
PaymentAccount: f.paymentAccount,
SMSCode: smsCode,
}
}