TDD核心概念 #
测试驱动开发(TDD)的核心是先写测试,后写代码。
三步循环 #
- RED(红): 写一个失败的测试
- GREEN(绿): 写最少代码让测试通过
- 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优势 #
- 速度快: Mock测试 ~1ms vs 真实DB ~500ms
- 隔离性: 每个测试完全独立
- 异常模拟: 轻松测试错误场景
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