테스트 도구 실전 · 무엇을 테스트하나 · 讲解
커버리지는 목표가 아니라 지도다
一句话总结
覆盖率是一张告诉你哪些地方尚未检查的地图。一旦把 100% 当作目标,它就不再是地图,而会变成必须填满的格子;从那一刻起,什么都不验证的测试就会越来越多。
为什么需要它
你可能见过这样的测试。
def test_create_user(): u = create_user("kim") assert u is not None这个测试执行了 create_user 的每一行,却什么都没有确认。覆盖率达到 100%,即使用户名没有保存,测试也会通过。
相反,真正危险的地方通常位于覆盖范围之外:错误处理分支、边界值、并发执行。这些地方因为编写测试麻烦而被跳过,也正因如此,缺陷会在其中生存。
优先测试什么
优先级应按照出错时的代价决定。
| 优先级 | 测试对象 | 原因 |
|---|---|---|
| 1 | 资金、权限、数据删除 | 出错后无法挽回 |
| 2 | 分支较多的纯函数 | 能以低成本验证大量情况 |
| 3 | 边界值和错误路径 | 人们很少主动尝试这些情况 |
| 4 | 一条集成路径 | 确认各组件能否连接起来 |
| 5 | 界面 | 成本最高,也最容易损坏 |
这正是测试金字塔所表达的原则——底部(单元测试)要宽,顶部(E2E)要窄。
如何阅读覆盖率
不要只看数字,要看缺失的行。
pytest --cov=mymod --cov-report=term-missingName Stmts Miss Cover Missingmymod.py 42 6 86% 17-19, 28, 51-52答案就在 Missing 中。如果 17–19 行是错误处理,就应该补上测试;如果 51–52 行只是日志,则可以不补。判断必须由人完成。
启用分支覆盖率后会更加准确。
pytest --cov=mymod --cov-branch如果只走过 if x: 为真的路径,行覆盖率可能是 100%,分支覆盖率却只有 50%。
常见误解
“测试通过就说明程序正确”——测试只能证明缺陷存在,不能证明缺陷不存在。通过只代表“在我想到的情况里是正确的”,没有更多含义。
“慢测试也算测试”——测试太慢,人们就不会运行。不运行的测试比不存在的测试更糟(它会让人误以为系统已经被验证)。所有单元测试都应在几秒内结束。
选择测试对象的标准
不可能测试所有内容。应从两个维度判断——出错造成的损失和出错概率。
| | 经常变化 | 几乎不变 |
|---|---|---|
| 损失很大 | 必须测试,这是最高优先级 | 应测试,用于防止回归 |
| 损失很小 | 可以不测试 | 不测试 |
支付、认证、数据删除位于左上角;界面文案和日志格式位于右下角。为了提高覆盖率数字而填满右下角,只会增加维护成本,事故却不会减少。
测试金字塔及其反例
基本形态是把缓慢昂贵的测试放在上方,把快速便宜的测试放在下方。
/\ E2E — 느리다(분), 잘 깨진다, 그러나 진짜를 본다 / /----\ 통합 — DB·큐를 실제로 띄운다(testcontainers) / /--------\ 단위 — 밀리초, 로직만不过,这种形态并非永远正确。对于逻辑很薄、集成很厚的服务(CRUD API、数据流水线),单元测试几乎没有价值。用模拟(mock)伪装数据库,反而无法发现真正的 SQL 错误。这类系统应当增加集成测试的比重。
比形态更重要的是“这个测试失败时,是否代表真实问题”。只要稍微修改实现就会失败的测试,会阻碍重构。这种测试比没有测试更糟。
好测试的三个特征
具有确定性。相同输入必须得到相同结果。依赖时间、随机数或执行顺序会造成间歇性失败,而间歇性失败很快就会被忽略。时间应通过参数注入(clock 参数),随机数应固定种子。
彼此独立。无论改变顺序,还是只运行其中一个,都必须通过。如果依赖前一个测试留下的数据,就无法并行执行。
名称能够表达意图。不要写 test_1,而应写 `test_returns_404_when_order_belongs_to_
another_user`。仅阅读失败列表,就应该知道哪里出了问题。
# 시각을 주입하면 결정적이 된다def is_expired(token, now=None): now = now or datetime.now(timezone.utc) return token.exp < nowdef test_expired_token_is_rejected(): t = Token(exp=datetime(2026, 1, 1, tzinfo=timezone.utc)) assert is_expired(t, now=datetime(2026, 1, 2, tzinfo=timezone.utc))实务中真正重要的事
测试的价值取决于:失败时能否立即看出原因。
# 나쁨 — 왜 틀렸는지 모른다assert result == expected# 좋음 — 무엇이 다른지 보인다assert result.status == 200, f"응답: {result.status} {result.body[:200]}"pytest 会自动显示 assert a == b 两侧的值。因此,与其编写很长的消息,不如把条件拆得更细。