Skip to content

🧪 测试策略

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 命令层

性能基准

指标目标值测量方法
单元测试执行时间< 5sgo test ./...
集成测试执行时间< 30sgo 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 ./...

📚 学习资源

测试相关文档

项目测试案例

外部参考


💡 测试建议: 测试不是负担,而是保证代码质量和开发效率的重要工具。遵循测试金字塔模型,重视单元测试,适量集成测试,少量端到端测试。

基于 MIT 许可证发布