Go语言中的TDD实践

TDD核心概念 #

测试驱动开发(TDD)的核心是先写测试,后写代码

三步循环 #

  1. RED(红): 写一个失败的测试
  2. GREEN(绿): 写最少代码让测试通过
  3. REFACTOR(重构): 在测试保护下优化代码

Go语言TDD实践 #

基础示例 #

// calculator_test.go - 先写测试
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

// calculator.go - 后写实现
func Add(a, b int) int {
    return a + b
}

表驱动测试(Go惯用法) #

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数相加", 2, 3, 5},
        {"零加零", 0, 0, 0},
        {"负数相加", -1, -1, -2},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("got %d, want %d", result, tt.expected)
            }
        })
    }
}

数据库查询的TDD #

方案1:接口+Mock(推荐) #

定义Repository接口 #

// interfaces.go
type BillRepository interface {
    GetByID(id string) (*BillRecord, error)
    Save(record *BillRecord) error
    GetTotalAmount(userName string) (float64, error)
}

真实实现 #

// bill_repository.go
type billRepository struct {
    db *gorm.DB
}

func (r *billRepository) GetByID(id string) (*BillRecord, error) {
    var record BillRecord
    err := r.db.Where("trade_no = ?", id).First(&record).Error
    return &record, err
}

Mock实现 #

// bill_repository_mock.go
type MockBillRepository struct {
    records map[string]*BillRecord
}

func (m *MockBillRepository) GetByID(id string) (*BillRecord, error) {
    if record, ok := m.records[id]; ok {
        return record, nil
    }
    return nil, gorm.ErrRecordNotFound
}

func (m *MockBillRepository) Save(record *BillRecord) error {
    m.records[record.TradeNo] = record
    return nil
}

测试中使用Mock #

func TestCalculateUserExpense(t *testing.T) {
    // 创建Mock
    mockRepo := NewMockBillRepository()
    
    // 准备数据
    mockRepo.Save(&BillRecord{
        TradeNo:          "001",
        UserName:         "张三",
        Amount:           "100.50",
        IncomeOrExpenses: "支出",
    })
    
    // 测试业务逻辑
    service := NewBillService(mockRepo)
    total, err := service.CalculateUserExpense("张三")
    
    // 断言
    assert.NoError(t, err)
    assert.Equal(t, 100.50, total)
}

方案2:内存数据库 #

func SetupTestDB() *gorm.DB {
    // SQLite内存数据库
    db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    db.AutoMigrate(&BillRecord{})
    return db
}

func TestRepository(t *testing.T) {
    db := SetupTestDB()
    repo := NewBillRepository(db)
    
    // 准备数据
    db.Create(&BillRecord{TradeNo: "001"})
    
    // 测试查询
    record, err := repo.GetByID("001")
    assert.NoError(t, err)
    assert.NotNil(t, record)
}

测试分层策略 #

Handler → Service → Repository → DB
   ↓         ↓          ↓
 Mock      Mock     真实DB测试
  • Repository层: 用真实数据库测试SQL正确性
  • Service层: Mock Repository,测试业务逻辑
  • Handler层: Mock Service,测试HTTP处理

Mock优势 #

  1. 速度快: Mock测试 ~1ms vs 真实DB ~500ms
  2. 隔离性: 每个测试完全独立
  3. 异常模拟: 轻松测试错误场景
type ErrorMockRepo struct{}

func (m *ErrorMockRepo) GetByID(id string) (*BillRecord, error) {
    return nil, errors.New("数据库连接失败")
}

TDD适用场景 #

适合TDD #

  • ✅ 复杂业务逻辑
  • ✅ 纯函数(明确输入输出)
  • ✅ 核心算法

不适合TDD #

  • ❌ UI界面代码
  • ❌ 数据库配置
  • ❌ 第三方API调用

核心原则 #

测试不是为了找Bug,测试是为了设计好代码

当写测试困难时,说明代码设计有问题:

  • 函数太大 → 拆分
  • 依赖太多 → 用接口
  • 逻辑混杂 → 分层

实用工具 #

# 运行测试
go test ./...

# 覆盖率报告
go test -cover ./...

# 表驱动测试 + 详细输出
go test -v

# 推荐测试库
go get github.com/stretchr/testify/assert
go get github.com/golang/mock/mockgen