单元测试实战:从「补测试」到用测试驱动设计

举报
yd_237615889 发表于 2026/09/16 03:14:12 2026/09/16
【摘要】 单元测试实战:从「补测试」到用测试驱动设计覆盖率报表挺好看,却没人敢重构——一改实现几十个测试变红。这种测试不叫安全网,叫负债。这篇讲清测试买什么、测什么不测什么、Mock 的边界、覆盖率的陷阱,以及怎么从遗留代码里长出可用的测试。工程实践手记 ·2026-09-16 ·约 12 分钟阅读一、先想清楚:测试买的是「改代码的底气」很多团队的测试现状是:覆盖率报表挺好看,但没人敢重构——因为一改...

单元测试实战:从「补测试」到用测试驱动设计

覆盖率报表挺好看,却没人敢重构——一改实现几十个测试变红。这种测试不叫安全网,叫负债。这篇讲清测试买什么、测什么不测什么、Mock 的边界、覆盖率的陷阱,以及怎么从遗留代码里长出可用的测试。


工程实践手记 ·2026-09-16 ·约 12 分钟阅读

一、先想清楚:测试买的是「改代码的底气」

很多团队的测试现状是:覆盖率报表挺好看,但没人敢重构——因为一改实现,几十个测试变红。测试的收益只有三条,且都和「改」有关:重构有底气(改完跑一遍,行为没变就敢提交)、回归有防线(新改动弄坏老功能,五分钟内知道)、文档可执行(测试是唯一不会过期的文档——它错了会红,文档错了没人知道)。判断一个测试值不值,只需问一句:它会在我改坏东西的时候变红吗?不会,就是摆设。

好的测试套件有两个特征:改坏东西时它会红,重构实现时它不会红。
· · ·

二、测试金字塔:先分配「测什么」

层级 数量占比 速度 测什么
单元测试 最多(约 70%) 毫秒级 业务规则、边界、异常路径
集成测试 中等(约 20%) 秒级 模块间协作、数据库、外部适配
端到端测试 最少(约 10%) 分钟级 关键用户旅程(下单、支付)

反模式叫「冰淇淋筒」:底层单元测试很少,顶层 E2E 一大堆——结果是跑得慢、坏得勤、修得累,最后被整个禁用。把断言的重心放在金字塔底部。

三、好测试的五个标准

  • 快:单元测试以毫秒计——慢测试没人愿意跑,不跑就等于没有
  • 独立:任意顺序、单独运行都能过;测试之间共享状态是灾难之源
  • 可重复:不依赖时间、网络、随机数——用固定时钟与种子
  • 自验证:结果只有通过或失败两种,不需要人看输出判断
  • 及时:写完逻辑就补,或干脆先写测试;事后补成本是当场补的三倍

四、测什么、不测什么

该测:业务规则(折扣怎么算、状态怎么流转、权限怎么判定)、边界条件(0、负数、空集合、时区跨天)、异常路径(下游超时、参数非法、并发冲突)。不该测:getter 与 setter 这类没有逻辑的搬运工、框架自身行为、私有实现细节(调用了几次内部方法)、日志文案与 UI 样式细节。写了只会拖累自己。

· · ·

五、Mock 的边界:Mock 越多,测试越脆

Mock 的适用对象只有一个:你控制不了、又不该在测试里碰的东西——网络、时钟、随机、第三方服务。两类常见的过度使用:Mock 自己的业务对象(测试变成「验证实现流程」,重构必红;更好的做法是用假对象 Fake——比如内存版仓储,行为真实、速度快);层层 Mock 穿透调用链(一个测试 mock 五个依赖,说明这坨代码耦合了五个关注点——这是设计问题,不是测试问题)。一句话原则:能不用 Mock 就不用,能注入真实实现(内存、本地)就别打桩。

六、覆盖率:90% 也可能一无所有

覆盖率衡量的是「代码被执行过」,不是「行为被验证过」:一行代码被跑过、但断言只写了「不抛异常」——覆盖率高,保护为零;反过来,核心规则只占 20% 代码,全覆盖它比 100% 覆盖率更有价值。正确用法:拿覆盖率当「找漏」的探照灯,而不是当 KPI。看报表里没被覆盖的分支,问自己「这里出问题会怎样」——而不是逼团队刷到 100%。

七、五个测试坏味道

  • 脆测试:断言内部实现(调用次数、私有方法),重构即红
  • 巨型测试:一个用例断言二十件事,失败时不知道坏在哪
  • 顺序依赖:测试 B 依赖测试 A 留下的数据,单独跑必挂
  • sleep 等待:用固定睡眠等异步结果,慢且随机失败——改成轮询加超时
  • 空断言刷分:为了覆盖率写「断言非空」,纯属自欺

一组对比看脆测试与行为测试的差别:

# ✗ 脆测试:断言内部实现细节
def test_discount_brittle(mocker):
    spy = mocker.spy(order, "_recalculate")
    service.apply_discount(order, 0.9)
    assert spy.call_count == 1      # 重构一次就红

# ✓ 行为测试:只断言可观察的结果
def test_discount_behavior():
    order = Order(items=[Item(price=100)])
    service.apply_discount(order, 0.9)
    assert order.total == 90        # 行为不变,重构随便改

八、遗留代码怎么补测试:从接缝处下刀

面对一坨没有测试的老代码,别想着「先补 80% 再说」:找接缝(seam)——把外部依赖(时钟、数据库、HTTP 客户端)改成可注入,接缝处就是测试的落点;从 bug 修起——每修一个 bug,先写一个复现它的测试,零风险、纯收益;从纯函数下刀——先测没有副作用的计算逻辑(价格计算、规则判定),最容易测、价值也最高;别追求全面覆盖——给「不敢动的核心逻辑」加上网,比给整个仓库刷覆盖率务实得多。

九、可测试性就是好设计

信号 说明什么 怎么改
需要 mock 五个依赖 职责过重 拆类,按单一职责切分
输入难构造 隐藏依赖(内部 new 了对象) 改成注入,依赖显式化
断言要访问私有状态 封装边界不清 通过公开行为断言
测试总是很慢 耦合了 IO 把 IO 推向外层

这也是「测试驱动设计」的真意:写测试时感到的别扭,就是设计需要改的地方。

十、落地清单

  • 新增业务逻辑必配单元测试;bug 修复先写复现测试
  • 断言可观察行为,不断言内部实现
  • 只 mock 外部世界;业务协作用真实对象或内存版 Fake
  • 覆盖率只用来找漏,不设 KPI 硬指标
  • 单元测试保持毫秒级,超时的要么改设计要么降级为集成测试
  • 每月清理一次常红的脆测试:修或删,别养着

速查卡

问题 处方
一重构测试就红 断言改行为,别断言实现
测试慢到没人跑 隔离 IO,把慢的降级为集成测试
覆盖率好看但没用 看分支与断言质量,覆盖率只用来找漏
Mock 越写越多 说明耦合太重,先改设计
遗留代码无从下手 从接缝、bug、纯函数三处开刀
测试偶尔失败 查顺序依赖与 sleep,改为独立与轮询

写在最后

测试的价值不在「有多少个」,而在「它保护了什么」。一套好的测试套件有两个特征:改坏东西时它会红,重构实现时它不会红。达不到这两条,先别急着加数量——回头看看断言写在哪一层。

下一步 · 今天修的第一个 bug 先写复现测试
零风险纯收益的补测试姿势:先红(复现)、再绿(修复)、留着防复发。系列下一篇候选:微调 vs RAG 选型、分布式事务、语义化版本自动发版。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。