软件分析与设计笔记 14:软件测试:层级、方法与等价类划分
「我们测过了」这句话问题在哪:为什么静态评审和动态测试缺一不可,五个测试层级各自验证什么,白盒与黑盒的差别,以及用等价类划分和边界值把无限输入变成有限用例的方法。
本文是 《软件分析与设计笔记》 系列的第 14 篇,共 28 篇
这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的。理解有偏差的地方欢迎指出。
贯穿案例:片区家电维修派单系统。这一篇讲怎么证明「它真的做到了」—— 重点是 27 篇考试里那道 4 分的测试用例题。
0 「我们测过了」——那测的是什么
交付前总会听到这句话。但接着问三个问题,答案往往就不那么确定了:
- 测了哪些层级?只测了单个方法能跑,还是连起来也测过?
- 用了哪些方法?只测了正常输入,还是异常输入也试过?
- 依据什么数据?随手试的,还是照着约束设计出来的?
放到贯穿案例里试一次。如果你做了「申请配件」这个功能,随口说「测过了」,那下面这些情况测了吗:
- 数量填 0 会怎样?
- 数量填 21(上限是 20)会怎样?
- 工单状态不是「进行中」时提交会怎样?
- 供应商接口超时了会怎样?
- 两个工程师同时申请同一个配件(只剩 1 个)会怎样?
这五条里只要有一条没测,「测过了」就不成立。这一篇要把这三件事讲清楚。先建立一个基本认识:测试的目的是发现缺陷(defect),不是证明没有缺陷。测试通过只能说明「这组输入下没出问题」,不能说明系统是对的。
1 为什么只测不够:评审与测试
1.1 评审与测试的分工
有一类缺陷,光靠运行程序是发现不了的:
- 需求漏写了一个分支 —— 程序按现在的需求跑得很好。本例:「工程师请假了,已派的单怎么办」需求里没写;
- 设计里两个模块职责重叠 —— 代码能跑,只是以后改起来很痛苦;
- 文档里的术语前后不一致 —— 甚至没有代码。
对付这类问题要用评审(review)。它和测试的分工是:
| 评审 | 测试 | |
|---|---|---|
| 是否运行程序 | 否,属于静态测试(static testing) | 是,属于动态测试(dynamic testing) |
| 主要发现 | 需求、设计、文档层面的缺陷 | 实现层面的缺陷 |
| 时机 | 越早越好 | 代码可运行之后 |
评审之所以重要,是因为缺陷发现得越晚,修复成本越高 —— 在需求阶段改一句话,和在测试阶段改一句话,代价差一个数量级。
本例里有个具体的账:如果「工程师请假怎么办」这个缺口没在需求评审时发现,等到上线后才发现,改的不只是一个分支,而是工单状态机要加一个状态、还要能回退 —— 那是两三天的活。
1.2 验证与确认
再补充两个容易混的词:
- 验证(verification) —— 我们有没有正确地构建产品?(符合规格说明)
- 确认(validation) —— 我们有没有构建正确的产品?(符合用户需要)
前者是「照着图纸做对了吗」,后者是「图纸本身对吗」。
放到本例:「查询响应 2 秒内完成」测过了 → 这是验证。但客服小林用了之后还是抱怨「找不到要派的单」→ 说明确认没过,因为图纸本身画错了(她需要的不是快,而是筛选)。
2 五个测试层级
2.1 五个层级各自验证什么
| 层级 | 验证什么 | 谁来测 | 本例的例子 |
|---|---|---|---|
| 单元测试(unit testing) | 单个方法或类的行为 | 开发 | PartUsage.calculateAmount() 算得对不对 |
| 集成测试(integration testing) | 模块之间的接口与协作 | 开发 / 测试 | 申请配件时,库存扣减与出库单创建是不是同一个事务 |
| 系统测试(system testing) | 整个系统的功能与非功能表现 | 测试 | 早高峰 300 人同时在线,派单是否还成功 |
| 验收测试(user acceptance testing, UAT) | 是否满足业务需要 | 用户 / 业务方 | 小林真的能用它派完一天 80 单吗 |
| 部署测试(deployment testing) | 在真实环境里能否正常安装运行 | 运维 / 测试 | 工程师的手机在 4G 弱网下能不能回填 |
第四行特别值得注意。小林是唯一能判断 UAT 过没过的人,因为她才是那个一天派 80 单的人。如果让没派过单的人去验收,通过与否没有意义。
2.2 金字塔形状说明了什么
金字塔的形状本身就是结论:底层的测试应该最多、最便宜、跑得最勤;上层的测试少而贵。一个健康项目的表现是「大部分缺陷在单元测试阶段就被拦住」。反过来,如果缺陷总是到 UAT 才暴露,那就说明底层测试做得太少。
本例里最容易缺底层的正是金额与时长的计算。配件金额、上门时长、超时判定这三块逻辑如果没单元测试,等到 UAT 才发现算错,改起来要重跑一整套流程。
3 白盒与黑盒
3.1 白盒与黑盒的差别
白盒测试(white box testing) 看内部逻辑:语句有没有都执行到、分支有没有都走到。它适合验证「代码写对了没有」,实施者需要懂代码。
黑盒测试(black box testing) 只看输入和输出,不看实现。它适合从使用者角度验证「行为对不对」,业务方也能参与。
本例里「申请配件」这段逻辑,两种测法的分工很清楚:
| 测什么 | |
|---|---|
| 白盒 | 「库存充足」和「库存不足」两个分支都走到了吗?异常处理有代码覆盖吗? |
| 黑盒 | 数量填 0 / 21 / 5,结果对不对?不关心代码怎么写的 |
两者有一个共同局限:都只能覆盖到你想到的情况。想不出某个输入组合,就永远测不到它。所以测试用例的设计方法比测试工具更重要。
3.2 测试覆盖率的分量
这也解释了测试覆盖率(test coverage)这个指标的分量:它衡量你设计的用例触及了多少代码或需求。但要注意,覆盖率只说明「被执行过」,不等于「被验证正确」—— 覆盖率 100% 也不能保证没有缺陷。
一个具体的反例:某段代码 if (qty > 20) return error; —— 你用 qty=25 测了一次,覆盖率是 100%,但你没测 qty=20(边界,应该通过)。代码跑过了,但不代表跑对了。
4 把无限输入变成有限用例:等价类划分
4.1 它的基本思路
一个输入框能接受的值是无限的,不可能一个个试。等价类划分(equivalence class partitioning) 的思路是:把输入空间划成若干段,每段里任意一个值的效果是一样的 —— 那每段测一个代表值就够了。
4.2 三种划分方式
常见的三种划分方式:
① 按值域划分。 本例「申请数量」允许 1–20,那么:
- 有效等价类:1–20 内取一个(例如 12);
- 无效等价类:小于 1(例如 0)、大于 20(例如 21)。
注意有效类里最好取三个:边界值 1、中间值 12、边界值 20。因为 > 写成 >= 这类错误只在边界暴露。
② 按输入个数划分。 例如一张工单至少要有一条配件消耗:
- 有效:1 条、5 条;
- 无效:0 条、极大条数(例如 200 条)。
③ 按异常条件(exception condition)划分。 空值、超长字符串、非法字符、并发提交、重复提交。
本例里第 ③ 类最容易被漏掉,但它恰恰最真实:
- 同一张工单被两个客服同时派给不同的人;
- 工程师连点两次「提交申请」(网络慢,他以为没反应);
- 配件编号输入了不存在的型号。
4.3 边界值分析
在值域的边界上还有个配套技巧 —— 边界值分析(boundary value analysis):出错往往发生在边界上(< 写成 <=、0 和 1 的差别),所以在每个等价类的边界上下各取一个值,命中率特别高。
本例「数量 1–20」的完整取法:
| 值 | 属于 | 期望结果 |
|---|---|---|
| 0 | 下边界外 | 报错 |
| 1 | 下边界上 | 通过 |
| 12 | 中间 | 通过 |
| 20 | 上边界上 | 通过 |
| 21 | 上边界外 | 报错 |
五个值,覆盖三件事:两个边界内外、一个中间值。这比随手填一个 999 有价值得多 —— 因为 999 和 21 是同一个等价类,多测一遍没有新信息。
5 测试准备:四样东西
5.1 四样东西
设计好用例之后,还需要准备:
- 测试用例(test case) —— 输入、预期输出、前置条件,三者缺一不可;
- 测试数据(test data) —— 符合用例要求的实际数据。边界数据要专门准备(例如「刚好 20 个」);
- 测试环境(test environment) —— 与生产尽量接近的环境。环境不一致造成的假问题,非常消耗排查时间;
- 测试步骤(test procedure) —— 用例的执行顺序。有些用例有先后依赖(先建工单,才能测申请配件)。
第二样本例里有个坑:测「库存不足」需要一个库存真的不足的配件。如果你在测试环境里用的是充足的数据,这条用例根本跑不到那个分支。所以测试数据要专门造「只剩 1 个」的库存。
5.2 功能测试与非功能测试
测试分两类:功能测试(functional testing) 验证系统做什么,非功能测试 验证做得多好(性能、安全、可靠性,见笔记 15)。
6 测试在敏捷里怎么放
6.1 测试成为迭代的一部分
迭代式开发里,测试不再是项目末尾的一个阶段,而是每个迭代的一部分,也就是所谓的敏捷测试周期(Agile testing lifecycle):
- 单元测试是「完成定义」的一部分(笔记 03)—— 没测试就不算完成;
- 每次改动后都要跑一遍已有的测试,确认没弄坏别的东西,这叫回归测试(regression testing)。它通常靠自动化来保证;
- 测试用例的来源变了:以前从需求文档推,现在直接从验收标准推 —— 一条验收标准对应至少一个测试用例。
这个变化带来一个好处:如果一条验收标准写不出对应的测试用例,那多半是这条需求本身没写清楚。
本例里 07 篇那条验收标准「每个时段标注空闲/已排/满档三态」,直接就能翻出三条测试用例(各验一态)+ 一条边界(无排班数据)。翻不出来就说明标准没写清。
本节自测
- 验证(verification)和确认(validation)的区别是什么?
- 哪些缺陷只能靠评审发现,测试发现不了?举出本例中的一个。
- 测试金字塔为什么是「下宽上窄」?如果缺陷老是在 UAT 才暴露,说明什么?
- 某字段允许 1–120,用等价类划分和边界值分析,至少要准备哪些测试数据?
- 为什么说「写不出测试用例的验收标准,说明需求有问题」?
- 本例「申请配件」的数量上限是 20,为什么只测 21 不够,还要测 20?
- 本例要测「库存不足」这个分支,测试数据上有什么特别要求?
参考答案
- 验证是「照着图纸做对了吗」(符合规格),确认是「图纸本身对吗」(符合用户需要)。本例:「2 秒内完成」测过是验证;但小林仍抱怨「找不到要派的单」说明确认没过(§1.2)。
- 需求、设计、文档层面的缺陷。本例中最典型的是「工程师请假了,已派的单怎么办」—— 需求里没写,程序按现有需求跑得很好,测不出问题(§1.1)。
- 因为底层测试多、便宜、跑得勤,上层少而贵;健康项目的缺陷大部分在单元层被拦住。若缺陷总在 UAT 暴露,说明底层测试做得太少(§2.2)。
- 至少:0(下边界外)、1(下边界上)、60(中间)、120(上边界上)、121(上边界外) —— 五个值,覆盖两个边界内外加一个中间值(§4.3)。
- 因为验收标准与测试用例是一对一推导关系。一条标准翻不出用例,说明它本身含糊、没有可观测的结果(§6.1)。
- 因为
>写成>=这类错误只在边界暴露。测 21 只能证明「超上限会报错」,测 20 才能证明「正好到上限时应通过」(§4.3、§3.2)。 - 必须专门造一个库存不足的配件(例如只剩 1 个)。如果测试数据里库存都充足,这条用例根本跑不到那个分支(§5.1)。
术语表
| 术语 | 中文 | 一句话解释 |
|---|---|---|
| verification | 验证 | 是否按规定构建了产品,对着规格检查 |
| validation | 确认 | 是否构建了正确的产品,对着用户需要检查 |
| review | 评审 | 不运行程序、靠人检查发现缺陷的方式 |
| static testing | 静态测试 | 不执行代码的检查,如评审与静态分析 |
| dynamic testing | 动态测试 | 实际运行程序来发现缺陷 |
| unit testing | 单元测试 | 验证单个方法或类的行为 |
| integration testing | 集成测试 | 验证模块之间的接口与协作 |
| system testing | 系统测试 | 验证整个系统的功能与非功能表现 |
| user acceptance testing(UAT) | 验收测试 | 由业务方确认是否满足需要 |
| deployment testing | 部署测试 | 验证在真实环境中的安装与运行 |
| white box testing | 白盒测试 | 基于内部逻辑设计的测试 |
| black box testing | 黑盒测试 | 只看输入输出的测试 |
| equivalence class partitioning | 等价类划分 | 把输入空间分段,每段取一个代表值 |
| boundary value analysis | 边界值分析 | 在等价类边界上下取值测试 |
| exception condition | 异常条件 | 空值、非法输入、并发等异常情形 |
| test case | 测试用例 | 输入、预期输出与前置条件的组合 |
| test data | 测试数据 | 执行用例所需的实际数据 |
| test environment | 测试环境 | 尽量接近生产的运行环境 |
| test procedure | 测试步骤 | 用例的执行顺序与依赖 |
| functional testing | 功能测试 | 验证系统做什么 |
| regression testing | 回归测试 | 改动后重跑既有测试,确认没弄坏别处 |
| test coverage | 测试覆盖率 | 测试触及代码或需求的比例 |
| defect | 缺陷 | 与预期不符的系统行为 |
| Agile testing lifecycle | 敏捷测试周期 | 测试融入每个迭代而非独立阶段 |