上一篇写的是一份能交给别人执行的用例表。但用例表不回答一个更靠前的问题:这些用例该对着什么跑、由谁跑?
同一个“会员折扣算错”的问题,写成单元测试几十毫秒就跑完,失败时直接指到哪一行;写成从下单页点到支付成功的端到端用例要跑两分钟,失败时你只知道“金额不对”,还得自己找是哪一层错的。它们能抓到的缺陷也不同:有的事只有把系统连起来才会出现,有的事你把系统点一百遍也抓不到。
测试层级切的就是这件事。单元、集成、系统、验收这四层,切的是同一条链上的四段接缝,区别只有一条:在这一层里,哪些东西是真的,哪些是你摆上去的假设。
层级也不等于测试类型。性能、兼容、安全说的是“测什么特性”,可以出现在任何一层;层级说的是“被测对象有多大” 1。先把四层的全貌摆出来:
| 层级 | 被测对象 | 拿什么当依据 | 典型能抓到的缺陷 | 通常由谁做 |
|---|---|---|---|---|
| 单元(组件) | 一个函数、类或模块,其余部分换成替身 | 代码、详细设计、数据模型 | 算错、分支写反、边界没处理 | 写这段代码的开发者 |
| 集成 | 两个真实组件之间的调用与数据 | 架构图、接口与协议说明 | 字段名或类型对不上、事务没提交、超时没处理 | 开发者或测试人员 |
| 系统 | 整个系统的端到端行为和特性 | 需求、用例、状态图、用户手册 | 串起来才出现的错,界面与数据不一致 | 独立测试人员 |
| 验收 | 系统是否满足业务需要、能不能上线 | 业务流程、业务需求、合同与法规 | 需求本身写错或没写清、业务上不可接受 | 真实用户、业务方 |
下面用一个例子走完四层:会员下单享折扣,银卡九五折、金卡九折,折后满 99 元免运费。
单元层:其余世界换成了替身,这是它快的原因,也是它的盲区
要单独测一个 discount(price, levelCode) 函数,得先让它跑起来。它内部会去查用户等级,而单元测试里没有数据库,于是你写一个假对象,让它直接返回 "GOLD" 就完了。这种假对象就是 测试替身:不连真东西,按你设定的样子返回结果。
速度快、定位精确都来自这一点:没有网络,没有界面,失败就指到那个函数。
盲区也来自这一点。替身的行为是你写的,也就是你的假设。 如果真实用户服务返回的等级是数字 3,或者字段名叫 member_level 而不是 levelCode,你的单元测试永远通过,因为它从来没跟真实服务说过话。它证明的是“这段代码符合我对别人的假设”,不是“别人真的这么返回”。所以单元测试铺得再密,也回答不了下一层的问题。
集成层:只验两个真实组件之间的接缝
把折扣服务、用户服务、数据库这三样都换成真的,让它们互相调用一次,这就是集成测试。它不重复验各自的内部逻辑——单测验过的折扣算法在这里不用再验——只管接缝:字段传过来是什么类型、名字对不对、事务有没有提交、超时和重试有没有处理。
上一节那个类型不一致的问题,只有到这一层才会暴露,因为两边都是真的,假设被换成了事实。集成层常用的做法是“只让相邻的两个组件是真的,其余继续用替身”,代价是别的接缝上的问题仍然测不到。
系统层:整条链路都是真的,但你能控的和能看的都变少了
一个功能真正的风险,常出在“几条接缝各自都对、串起来却不对”的地方。
接着上面的例子:折扣服务算应付金额,运费服务判断要不要免运费。前者用折后价判断满 99 的门槛,后者用折前价判断。两个服务单测都过,接缝上的数据也都传对了,可是用户下单时,页面上显示的优惠明细是一个数,实际扣款是另一个数。只有把整条链路跑一遍、把页面金额和实际收款比一比,才看得见它。
系统层的依据是需求、用例、状态图这些描述“整个系统应该怎样”的东西,验的是整体行为和端到端任务,也包括性能、易用性这类必须在完整系统上才测得出的特性 1。代价是你能控制的输入变少、能看到的输出变粗:失败时它只说“金额不对”,不说哪儿不对。
验收层:它换了一个完全不同的问题
到这里必须说清一件容易被忽略的事:前三层用的都是同一份需求文档当尺子。 需求写错,系统测试会照着错的需求一路通过。
举例:公司政策其实是“折扣与满减不可同享”,但需求文档没写,于是两者叠加了。单元、集成、系统三层全部通过,因为它们验的都是“有没有按文档实现”。这类问题只有换一个问题才能回答:我们做出来的这个东西,是不是业务真正要的东西。
这就是第一篇讲过的区分:前三层主要在回答“做得对不对”(验证),验收层回答“做的是不是对的东西”(确认)。ISTQB 把验收测试的目的写成“确认系统满足用户的业务需要、并证明它已可部署”,而且最好由预期用户来做 1。所以验收层不重复前三层的检查,它拿业务流程、真实数据和业务判断当依据。前三层全绿,也不等于可以上线。
金字塔:底宽顶窄,不是因为低层更重要
写成自动化,这三类测试的单价差很多:单元测试毫秒级、失败指到函数、不依赖网络和界面;端到端测试要把整个应用跑起来、点按钮、等页面,慢几个数量级,界面一改还可能挂掉 3。
同时,一条单元测试只覆盖一个判断,一条端到端测试却穿过所有层。覆盖同样一片范围,低层需要的条数多,高层需要的条数少。 一边是单价,一边是条数,两头一起决定了形状:单元最多,中间的接口层次之,UI 层最少。这就是 Mike Cohn 在《Succeeding with Agile》里提出的测试金字塔 2,它讲的是自动化测试该往哪一层投。
由此得到三条做法:能写成单元测试的不要放到 UI 层;单元测试覆盖不到的接缝,用集成测试补;UI 测试少而精,只覆盖必须端到端才能验的东西 3。对写用例的人还有一句更实用的:把一条用例尽量放低,低到那个风险仍然抓得住为止。 反过来,把自动化全堆在 UI 层,就是所谓“冰淇淋筒”,也是记录回放式 UI 自动化的常见结局 2。
这个形状有个前提:上层测试昂贵、缓慢、脆弱。Fowler 自己标出了例外——如果你的高层测试又快又稳、改动成本又低,低层就不必铺那么多 2。后来出现的测试奖杯、蜂巢模型,就是把这个成本收益判断换到别的技术栈里重算的结果 4,金字塔并不因此失效。
怎么决定一个功能该在哪一层测
把上面的内容合成三步:
- 先把风险写成“必须有哪样东西是真的”。 比如“用户等级字段的类型和代码预期一致”“页面显示的金额和实际扣款一致”“折扣不与满减叠加是公司政策”。
- 找最低的、能让这个条件真实存在的那一层。 只有当数据库是真的,才验得出字段类型;只有当整条链路串起来,才能比对页面金额和扣款;只有当业务方在场,才判得了政策。
- 高层只留“串起来才出现”和“业务上说不通”的东西。 其余的留在低层,高层才跑得快、失败时才说得清。
按这三步回看你手头的用例表,很多条目会自己找到该去的层——也会发现有些风险,四层里没有任何一层能替你回答。