名称: tdd-mastery 描述: 跨语言的测试驱动开发工作流程,基于红-绿-重构循环
TDD精通
核心循环:红-绿-重构
- 红 - 编写一个失败的测试,定义期望的行为
- 绿 - 编写最少的代码使测试通过
- 重构 - 清理代码,同时保持测试通过
永远不要在没有失败测试的情况下编写生产代码。每个循环应持续2-10分钟。
测试结构
一致使用Arrange-Act-Assert模式:
Arrange: 设置测试数据和依赖
Act: 执行被测试的行为
Assert: 验证预期结果
将测试命名为 test_<单元>_<场景>_<预期结果> 或 it("应该在<条件>时<行为>")。
Jest / Vitest 模式
describe("OrderService", () => {
it("should apply discount when order exceeds threshold", () => {
const order = createOrder({ items: [{ price: 150, qty: 1 }] });
const result = applyDiscount(order, { threshold: 100, percent: 10 });
expect(result.total).toBe(135);
});
it("should throw when applying discount to empty order", () => {
const order = createOrder({ items: [] });
expect(() => applyDiscount(order, defaultDiscount)).toThrow(EmptyOrderError);
});
});
使用 vi.fn() / jest.fn() 进行模拟。优先使用依赖注入而非模块模拟。使用 beforeEach 进行共享设置,永远不要在测试之间共享可变状态。
pytest 模式
@pytest.fixture
def db_session():
session = create_test_session()
yield session
session.rollback()
def test_create_user_stores_hashed_password(db_session):
user = UserService(db_session).create(email="a@b.com", password="secret")
assert user.password_hash != "secret"
assert verify_password("secret", user.password_hash)
@pytest.mark.parametrize("input,expected", [
("", False),
("short", False),
("ValidPass1!", True),
])
def test_password_validation(input, expected):
assert validate_password(input) == expected
使用 pytest.raises 处理异常。使用 conftest.py 共享fixture。使用 @pytest.mark.slow 标记慢速测试。
Go 测试模式
func TestParseConfig(t *testing.T) {
tests := []struct {
name string
input string
want Config
wantErr bool
}{
{"valid yaml", "port: 8080", Config{Port: 8080}, false},
{"empty input", "", Config{}, true},
{"invalid port", "port: -1", Config{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseConfig([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("ParseConfig() = %v, want %v", got, tt.want)
}
})
}
}
默认使用表驱动测试。在测试辅助函数中使用 t.Helper()。仅在团队已使用的情况下使用 testify/assert。
测试级别
| 级别 | 范围 | 速度 | 依赖 |
|---|---|---|---|
| 单元 | 单个函数/类 | <100毫秒 | 无(模拟所有) |
| 集成 | 模块边界 | <5秒 | 真实数据库,真实文件系统 |
| 端到端 | 完整用户流程 | <30秒 | 完整堆栈 |
比例目标:70% 单元,20% 集成,10% 端到端。
覆盖率规则
- 在CI中强制执行80%行覆盖率最低
- 跟踪分支覆盖率,不仅仅是行覆盖率
- 排除生成的代码、类型定义和配置文件
- 永远不要仅为达到覆盖率数字而编写测试;测试行为
# Jest/Vitest
vitest run --coverage --coverage.thresholds.lines=80 --coverage.thresholds.branches=75
# pytest
pytest --cov=src --cov-fail-under=80 --cov-branch
# Go
go test -coverprofile=cover.out -coverpkg=./... ./...
go tool cover -func=cover.out
模拟指南
- 在边界模拟:HTTP客户端、数据库、文件系统、时钟
- 永远不要模拟被测试的单元
- 对于仓库,偏好假对象(内存实现)而非模拟
- 断言行为,而不是模拟调用计数
- 使用
t.Cleanup/afterEach重置共享模拟
需避免的反模式
- 测试实现细节而不是行为
- 当代码被删除时测试仍通过(同义反复测试)
- 测试用例之间共享可变状态
- 忽略不稳定的测试而不是修复它们
- 直接测试私有方法
- 模糊意图的巨大测试设置