1.2 单元测试的界定与瞄准范围


本节摘要:单件体检的"单位"到底指的是什么,边界画在哪,单元测试与集成测试的分水岭是"隔离"而非"大小"。本节给出一张可操作的判定清单,并淘汰"函数多大算单元"这种错误纠缠。

为什么"单元"两个字吵不清

进阶群里隔三差五就有这样的争论:"一个 Service 类算不算单元?""一个跨了文件系统的工具函数算不算单元?"吵半天,谁也说服不了谁。原因在于很多人在用"代码体量"或者"文件层级"来定义单元,这是两个错的方向。体量大不代表不是单元,体量小也不代表是单元。真正的判据只有一个,而且它和代码行的关系是零——被测对象在运行它时是不是被完全隔离的

闭合,才是单元的精髓

我们把"单元"定义为:一段在执行时不需要任何真实外部世界(磁盘、网络、数据库、时钟、其他服务、全局状态)即可完成的逻辑。它可以是函数,是方法,也可以是协作紧密的一小组对象——只要这段被测行为在隔离环境里能忠实地跑起来,它就是合格的单元。

这就引出了单元测试与集成测试那道真正的界碑:不是被测代码的物理大小,而是它是否与外物解耦。同样一个"计算订单折扣"的函数,如果折扣规则全部写在纯内存里、输入输出都是参数,它是完美单元;一旦这个函数内部直接 new 了一个数据库连接去查会员等级,它就滑进了集成区——即便逻辑长得一模一样。所以单测里的说法"这函数依赖数据库就得用替身换掉",不是因为嫌数据库慢,而是因为一旦碰真库,你的测试就从"单件体检"变成了"件与库的拼装会诊"。

一张判定清单:什么算单元

实际操作中,拿下面的清单走一遍就能判断被测对象是否属于单元层。任意一条命中"否",就得想办法隔离,隔离不了就得承认它该被集成测试承包。

判据 单件体检要求 不满足时的处置
是否纯内存计算 不读写磁盘/网络 抽象成可注入的端口
时钟是否可拨 不使用真实当前时间 注入时钟接口
数据库是否隔离 用替身或内存库取代 归入集成层
语义是否确定性 相同输入必相同输出 拆出可测纯核,非纯逻辑另测
全局状态是否可控 不依赖隐藏全局变量 显式传入依赖
是否全自动可复跑 无需人和环境配合 引到更快、更稳的层

一条可运行的示例

下面这段折扣函数是典型"能当单元测"的,因为它只依赖参数,没有外物。以 Python + pytest 为例,跑完三秒内看到绿灯。

def apply_discount(amount: float, level: str) -> float: discounts = {"silver": 0.05, "gold": 0.10, "platinum": 0.20} rate = discounts.get(level, 0.0) return round(amount * (1 - rate), 2) def test_platinum_applies_twenty_percent(): assert apply_discount(100.0, "platinum") == 80.0 def test_unknown_level_gets_no_discount(): assert apply_discount(100.0, "guest") == 100.0

两个断言分别覆盖已知档位和未知档位这两条路径,纯内存、无副作用,改一行立即可再验。这就是单元层该有的样子。

瞄准范围:别把脏活硬塞给单件体检

虽然希望每个函数都可测,但有些逻辑天生不该靠单元层兜底。比如真正需要和数据库交互的仓储、需要真实签署的令牌签发、需要真实网络的第三方回调。对这些,硬把它当成单元只会得到两样东西:一堆浓到冲鼻的 Mock,和一个测不到真相的假绿灯。正确做法是承认它们属于集成层,把这一格让给第 3 章的手艺。单元测试的瞄准范围,是"能在内存里闭环"的那部分——它是全系统里数量最多、跑得最快的防线。

💡 一个判断捷径:如果为了测一个函数你不得不写超过十几个替身还在纠结,那大概率不是"单元测试难写",而是这段代码压根不属于单元层的瞄准范围。

边界部位:接口与触发器怎么归类

纯粹的函数好归类,但真实代码里总有几种"左右横跳"的部位值得单独定口径。其一,接口的"适配层"——只做参数格式转换、不碰外物的,可归单元;但如果它内部去解析一个从外部拿来的文件流,就滑向集成。其二,"入口触发器"——比如一个定时任务或网络回调的壳,它本身只有几行,大多是调别的;对这种,把壳里的逻辑剥出来当纯函数测,壳交给集成冒烟,不要硬把整段都按单元压。其三,"内部 I/O 包装"——比如封装了文件读写的小函数,除非能注入端口隔离,否则别当纯单元。

这条口径的意义在于:给每个项目一份书面的"单元/集成分界清单",比到写测试时才争论高效得多。把容易模糊的部位提前列出来,团队就有了共识地图,少了不少沟通成本。

一页对齐:单元测试的"在管/不在管"

在单元管的范围 不在单元管的范围
函数/方法的入参出参与分支 真实数据库读写与迁移
状态机与流转规则 真实网络请求与第三方回调
计算、判断、格式化逻辑 真实文件系统写入
纯业务规则的边界与等价类 真实时钟驱动的过期判断
与其他单件的协作(隔离下) 跨进程协议与消息队列

这张表可以作为团队评审的默认口径:某段代码落进右边一列,就默认它归集成层去管,别在单元层硬拗。它不是铁律,而是一个避免每日争论的通用协议。

常见的一个反例:纯函数的多面性

边界清单看久了,容易犯一个轴——"我以为这是纯函数,其实不是"。举个典型:一个读环境变量再返回配置的"getConfig"函数。表面上它只查内存字典,够像一个单元;可一旦它里面悄悄读了进程的环境变量、或是吃了文件里的 .env,它的输出就不再由入参唯一决定,而是被"外部世界"这根弦牵着走。同一段代码,在内存字典版是纯单元,在读文件版就滑进了集成区。差别不在函数名,而在它背地里捞了多少外物。

这类"以为纯、其实不纯"的错判,正是不做隔离、直接跑单元测试却偶尔绿偶尔红的常见来源。若某条单测在不同机器上结果不一样,第一反应别去解题目,而是回头查它是不是偷偷触到了环境变量或文件。把"纯"的定义磨清楚,能消灭一大半"偶发单测"的玄学。

本节要点回顾

  • 单元的精髓是闭合:执行时不依赖真实外物,就缺它隔离环境这点本质。
  • 分水岭是隔离不是体量:同段逻辑插了数据库就从单元滑进集成。
  • 判定清单兜底:内存、时钟、库、确定性、全局态、自动复跑六问。
  • 瞄准范围有限:数据/签名/网络类逻辑别硬拗单元,留给集成层。
  • 跑得快才配叫单元:测一次三秒内的才是合格的单件体检。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U