🧪 测试策略
lzt 项目采用全面的测试策略,确保代码质量和系统稳定性。本文档涵盖了从单元测试到集成测试的完整测试体系。
🎯 测试哲学
测试金字塔
我们遵循经典的测试金字塔模型,确保测试的有效性和效率:
核心原则
- 测试驱动开发 (TDD) - 先写测试,再写实现
- 快速反馈 - 测试执行时间短,反馈及时
- 独立性 - 测试之间相互独立,不相互影响
- 可重复性 - 测试结果稳定,可重复执行
- 覆盖率导向 - 确保关键代码路径被测试覆盖
📋 测试分层策略
1. 单元测试 (70%)
覆盖范围
- pkg/ 包 - 所有公共库必须有单元测试,覆盖率 ≥ 80%
- 核心业务逻辑 - 关键算法和业务规则
- 边界条件 - 异常输入和极限情况
- 错误处理 - 各种错误场景的处理
示例:Bubble 组件测试
go
func TestProcessStrings(t *testing.T) {
tests := []struct {
name string
input []string
expected int
hasError bool
}{
{
name: "normal processing",
input: []string{"task1", "task2", "task3"},
expected: 3,
hasError: false,
},
{
name: "empty input",
input: []string{},
expected: 0,
hasError: false,
},
{
name: "error in processing",
input: []string{"error_task"},
expected: 0,
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var processed int
processFunc := func(s string) error {
if s == "error_task" {
return errors.New("processing failed")
}
processed++
return nil
}
err := bubble.ProcessStrings(tt.input, processFunc)
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, processed)
}
})
}
}2. 集成测试 (20%)
数据库集成测试
go
func TestLedgerRepository_Create(t *testing.T) {
// 使用测试数据库
db := setupTestDB(t)
defer cleanupTestDB(t, db)
repo := NewLedgerRepository(db)
ledger := &Ledger{
Name: "Test Ledger",
Type: LedgerTypePersonal,
Currency: CurrencyCNY,
Description: "Test description",
}
err := repo.Create(context.Background(), ledger)
assert.NoError(t, err)
assert.NotEmpty(t, ledger.ID)
// 验证数据是否正确存储
retrieved, err := repo.GetByID(context.Background(), ledger.ID)
assert.NoError(t, err)
assert.Equal(t, ledger.Name, retrieved.Name)
}gRPC 服务集成测试
go
func TestLedgerService_Integration(t *testing.T) {
// 启动测试服务
server := setupTestServer(t)
defer server.Stop()
conn := setupTestClient(t, server.Addr())
client := pb.NewLedgerServiceClient(conn)
// 测试创建账本
req := &pb.CreateLedgerRequest{
Name: "Integration Test Ledger",
Type: pb.LedgerType_LEDGER_TYPE_PERSONAL,
}
resp, err := client.CreateLedger(context.Background(), req)
assert.NoError(t, err)
assert.NotNil(t, resp.Ledger)
assert.NotEmpty(t, resp.Ledger.Id)
}3. 端到端测试 (10%)
CLI 命令测试
go
func TestCLI_LedgerCommands(t *testing.T) {
// 设置临时环境
tmpDir := t.TempDir()
os.Setenv("LZ_STASH_CONFIG_DIR", tmpDir)
tests := []struct {
name string
args []string
expected string
}{
{
name: "init command",
args: []string{"ledger", "init"},
expected: "Database initialized successfully",
},
{
name: "server start",
args: []string{"ledger", "server", "--port", "0"},
expected: "Server started on port",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := executeCommand(t, tt.args...)
assert.Contains(t, output, tt.expected)
})
}
}⚡ 性能和基准测试
基准测试示例
go
func BenchmarkProgressBarRendering(b *testing.B) {
tasks := generateTestTasks(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
processFunc := func(task Task) error {
// 模拟轻量级处理
return nil
}
bubble.RunProgressWithOptions(tasks, processFunc, bubble.Options{
Mode: bubble.ViewportMode,
})
}
}
func BenchmarkConcurrentProcessing(b *testing.B) {
tasks := generateTestTasks(100)
benchmarks := []struct {
name string
workers int
}{
{"SingleWorker", 1},
{"FiveWorkers", 5},
{"TenWorkers", 10},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
bubble.ProcessConcurrent(tasks, processTask, bubble.Options{
Workers: bm.workers,
})
}
})
}
}内存基准测试
go
func BenchmarkMemoryUsage(b *testing.B) {
tests := []struct {
name string
taskCount int
mode bubble.DisplayMode
}{
{"Viewport_1K", 1000, bubble.ViewportMode},
{"Viewport_10K", 10000, bubble.ViewportMode},
{"FullOutput_1K", 1000, bubble.FullOutputMode},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
tasks := generateTestTasks(tt.taskCount)
var m1, m2 runtime.MemStats
runtime.ReadMemStats(&m1)
bubble.RunProgressWithOptions(tasks, processTask, bubble.Options{
Mode: tt.mode,
})
runtime.ReadMemStats(&m2)
b.ReportMetric(float64(m2.Alloc-m1.Alloc), "bytes/op")
})
}
}🛠️ 测试工具和框架
核心测试库
go
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)测试辅助函数
go
// 设置测试数据库
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
err = db.AutoMigrate(&Ledger{}, &Transaction{}, &Tag{})
require.NoError(t, err)
return db
}
// 生成测试数据
func generateTestTasks(count int) []bubble.Task {
tasks := make([]bubble.Task, count)
for i := 0; i < count; i++ {
tasks[i] = bubble.NewSimpleTask(
fmt.Sprintf("Task %d", i+1),
fmt.Sprintf("data_%d", i+1),
)
}
return tasks
}
// 执行 CLI 命令测试
func executeCommand(t *testing.T, args ...string) string {
cmd := exec.Command("go", append([]string{"run", "main.go"}, args...)...)
output, err := cmd.CombinedOutput()
require.NoError(t, err)
return string(output)
}📊 测试质量指标
覆盖率要求
| 包类型 | 覆盖率要求 | 说明 |
|---|---|---|
| pkg/ | ≥ 80% | 公共库强制要求 |
| internal/app/ | ≥ 70% | 业务逻辑核心 |
| internal/pkg/ | ≥ 80% | 基础设施组件 |
| cmd/ | ≥ 60% | CLI 命令层 |
性能基准
| 指标 | 目标值 | 测量方法 |
|---|---|---|
| 单元测试执行时间 | < 5s | go test ./... |
| 集成测试执行时间 | < 30s | go test -tags=integration |
| 内存分配 | < 100MB | 基准测试监控 |
| CPU 使用率 | < 80% | 并发测试监控 |
🔍 测试策略实践
TDD 开发流程
实践示例
go
// 第1步:编写失败的测试
func TestCalculateBudgetUsage(t *testing.T) {
budget := &Budget{Amount: 1000, UsedAmount: 300}
usage := CalculateBudgetUsage(budget)
assert.Equal(t, 30.0, usage.Percentage)
assert.Equal(t, 700, usage.RemainingAmount)
}
// 第2步:编写最少代码使测试通过
func CalculateBudgetUsage(budget *Budget) *BudgetUsage {
percentage := float64(budget.UsedAmount) / float64(budget.Amount) * 100
remaining := budget.Amount - budget.UsedAmount
return &BudgetUsage{
Percentage: percentage,
RemainingAmount: remaining,
}
}
// 第3步:重构优化
func CalculateBudgetUsage(budget *Budget) *BudgetUsage {
if budget.Amount == 0 {
return &BudgetUsage{Percentage: 0, RemainingAmount: 0}
}
percentage := float64(budget.UsedAmount) / float64(budget.Amount) * 100
remaining := budget.Amount - budget.UsedAmount
return &BudgetUsage{
Percentage: math.Round(percentage*100) / 100, // 保留2位小数
RemainingAmount: remaining,
}
}🚀 持续集成中的测试
GitHub Actions 测试流水线
yaml
name: Test Suite
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.24.3'
- name: Run unit tests
run: go test ./... -v -race -coverprofile=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
integration-tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test_db
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.24.3'
- name: Run integration tests
run: go test -tags=integration ./...
env:
DB_HOST: localhost
DB_USER: root
DB_PASSWORD: password
benchmark-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.24.3'
- name: Run benchmarks
run: go test -bench=. -benchmem ./...📚 学习资源
测试相关文档
- TDD 测试驱动开发 - 详细的 TDD 实践指南
- 单元测试最佳实践 - 编写高质量单元测试
- 集成测试策略 - 组件集成测试方法
- 基准测试指南 - 性能测试和优化
项目测试案例
- Go 测试最佳实践 - 完整的测试最佳实践指南
- Ledger 服务测试 - 完整的服务测试策略
外部参考
💡 测试建议: 测试不是负担,而是保证代码质量和开发效率的重要工具。遵循测试金字塔模型,重视单元测试,适量集成测试,少量端到端测试。