关于 clean-tests
clean-tests 是一项面向开发者的测试实践技能,帮助编写可靠且易维护的测试。内容涵盖覆盖率、边界情况、测试组织、失败模式和测试速度。
主要功能
- 01测试边界情况
- 02用覆盖率工具发现遗漏
- 03保留简单测试并处理跳过项
- 04分析失败模式
- 05每个测试围绕一个概念
- 06保持测试快速且独立
适用场景
- 01检查测试套件是否遗漏边界情况
- 02利用覆盖率结果定位未测代码
- 03诊断反复出现的测试失败
- 04改善测试速度和组织方式
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ ertugrul-dmr/ clean-code-skills --skill clean-tests安装后在 Claude Code 中输入/clean-tests确认已加载。
详细介绍
简洁测试
T1:测试不足
测试所有可能出问题的内容。将覆盖率工具作为参考,而不是目标。
# Bad - only tests happy path
def test_divide():
assert divide(10, 2) == 5
# Good - tests edge cases too
def test_divide_normal():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_negative():
assert divide(-10, 2) == -5
T2:使用覆盖率工具
覆盖率工具会指出测试策略中的缺口。不要忽视它们。
# Run with coverage
pytest --cov=myproject --cov-report=term-missing
# Aim for meaningful coverage, not 100%
T3:不要跳过简单测试
简单测试能记录行为并捕获回归问题。它们的价值远超成本。
# Worth having - documents expected behavior
def test_user_default_role():
user = User(name="Alice")
assert user.role == "member"
T4:被忽略的测试说明存在歧义
不要用 @pytest.mark.skip 来掩盖问题。要么修复测试,要么删除测试。
# Bad - hiding a problem
@pytest.mark.skip(reason="flaky, fix later")
def test_async_operation():
...
# Good - either fix it or document why it's skipped
@pytest.mark.skip(reason="Requires Redis, see CONTRIBUTING.md for setup")
def test_cache_invalidation():
...
T5:测试边界条件
Bug 常出现在边界处。要明确测试这些情况。
def test_pagination_boundaries():
items = list(range(100))
# First page
assert paginate(items, page=1, size=10) == items[0:10]
# Last page
assert paginate(items, page=10, size=10) == items[90:100]
# Beyond last page
assert paginate(items, page=11, size=10) == []
# Page zero (invalid)
with pytest.raises(ValueError):
paginate(items, page=0, size=10)
# Empty list
assert paginate([], page=1, size=10) == []
T6:全面测试 Bug 附近的情况
发现一个 Bug 时,要为所有类似情况编写测试。Bug 往往成簇出现。
# Found bug: off-by-one in date calculation
# Now test ALL date boundaries
def test_month_boundaries():
assert last_day_of_month(2024, 1) == 31 # January
assert last_day_of_month(2024, 2) == 29 # Leap year February
assert last_day_of_month(2023, 2) == 28 # Non-leap February
assert last_day_of_month(2024, 4) == 30 # 30-day month
assert last_day_of_month(2024, 12) == 31 # December
T7:失败模式能揭示问题
测试失败时,要观察其中的模式。它们往往指向更深层的问题。
# If all async tests fail intermittently,
# the problem isn't the tests—it's the async handling
T8:测试覆盖模式能揭示问题
查看哪些代码路径尚未测试。它们往往暴露设计问题。
# If you can't easily test a function, it probably does too much
# Refactor for testability
T9:测试应当快速
运行缓慢的测试不会被执行。将每个单元测试控制在 100 ms 以内。
# Bad - hits real database
def test_user_creation():
db = connect_to_database() # Slow!
user = db.create_user("Alice")
assert user.name == "Alice"
# Good - uses mock or in-memory
def test_user_creation():
db = InMemoryDatabase()
user = db.create_user("Alice")
assert user.name == "Alice"
测试组织
F.I.R.S.T. 原则
- 快速(Fast):测试应快速运行
- 独立(Independent):测试不应相互依赖
- 可重复(Repeatable):在任何环境中每次都得到相同结果
- 自验证(Self-Validating):自动通过或失败,无需人工检查
- 及时(Timely):在编写代码之前或同时编写,而不是事后编写
每个测试只验证一个概念
# Bad - testing multiple things
def test_user():
user = User("Alice", "[email protected]")
assert user.name == "Alice"
assert user.email == "[email protected]"
assert user.is_valid()
user.activate()
assert user.is_active
# Good - one concept each
def test_user_stores_name():
user = User("Alice", "[email protected]")
assert user.name == "Alice"
def test_user_stores_email():
user = User("Alice", "[email protected]")
assert user.email == "[email protected]"
def test_new_user_is_valid():
user = User("Alice", "[email protected]")
assert user.is_valid()
def test_user_can_be_activated():
user = User("Alice", "[email protected]")
user.activate()
assert user.is_active
快速参考
| 规则 | 原则 |
|---|---|
| T1 | 测试所有可能出问题的内容 |
| T2 | 使用覆盖率工具 |
| T3 | 不要跳过简单测试 |
| T4 | 忽略测试 = 存在歧义 |
| T5 | 测试边界条件 |
| T6 | 全面测试 Bug 附近的情况 |
| T7 | 查找失败模式 |
| T8 | 调试时检查覆盖率 |
| T9 | 测试必须快速(<100 ms) |
评论
支持 Markdown