《软件测试的艺术》读书笔记:用案例讲透测试

Glenford J. Myers, Tom Badgett, Corey Sandler, The Art of Software Testing, 3rd Edition, Wiley (2011/2012)。初版 1979 年由 Myers 写成,是软件测试领域最经典的书之一。

这篇笔记把全书拆成「一条核心信念 + 一套可操作方法 + 一堆真实案例」,尽量把书里的例子原样带出来,方便你直接照着用。

一个让人清醒的案例:三边判断程序(第 1 章)

书一上来就给读者一个小程序:输入三个整数,判断它们能否构成三角形,并区分等边 / 等腰 / 不等边(scalene)。

看起来五分钟能测完?作者列了 14 道自测题,答"是"得 1 分:

  1. 有没有一个有效的不等边三角形用例?(1,2,3、2,5,10 不算,它们根本构不成三角形)
  2. 有没有一个有效的等边三角形用例?
  3. 有没有一个有效的等腰三角形用例?(2,2,4 不算,它不是三角形)
  4. 有没有至少三个等腰用例,覆盖"两条相等边的三种排列"(3,3,4 / 3,4,3 / 4,3,3)?
  5. 有没有某条边为 0 的用例?
  6. 有没有某条边为负数的用例?
  7. 有没有"两边之和正好等于第三边"的用例(如 1,2,3——若程序判它是不等边三角形,就有 bug)?
  8. 第 7 类有没有覆盖三种排列(1,2,3 / 1,3,2 / 3,1,2)?
  9. 有没有"两边之和小于第三边"的用例(如 1,2,4、12,15,30)?
  10. 第 9 类有没有覆盖三种排列?
  11. 有没有全为 0 的用例(0,0,0)?
  12. 有没有非整数用例(如 2.5,3.5,5.5)?
  13. 有没有参数个数错的用例(只给两个而不是三个整数)?
  14. 每个用例,你是否同时写清了预期输出(而不只是输入)?

作者说:经验丰富的职业程序员,平均只拿 7.8 / 14 分。 目的就是击碎一个幻觉——连这种"玩具程序"都测不透,遑论十万行的空中交通管制系统、编译器或工资系统。第 14 题还藏了全书最重要的一条纪律:测试用例 = 输入 + 预期输出,两者缺一不可。

核心信念:测试是为了"找错",不是为了"证明对"(第 2 章)

初学者(和管理者)常见的错误定义:
- "测试是证明程序没有错误的过程。"
- "测试的目的是证明程序完成了它该做的功能。"

作者说这些全反了。正确的定义是:

测试,是以发现错误为目的而执行程序的过程。
(Testing is the process of executing a program with the intent of finding errors.)

为什么这个措辞如此重要?

  • 它决定你的取证倾向:若目标是"证明没错",人会下意识挑选几乎不会失败的输入;若目标是"证明有错",就会挑选更可能触发失败的输入。
  • 它是一场"破坏性"活动:测的人是在拆东西,而多数人天性爱造东西——这就是为什么测试"反人性、难受"。
  • 失败的用词要反转:项目经理常把"没找到错误的测试"叫"成功",把"发现新错误的测试"叫"失败"——完全说反了。一个发现新错误的测试用例,是有价值投资;真正"不成功"的用例,是执行了、结果也对、却什么错误都没查出。

两个精彩的类比:

  • 医生和化验:你感觉不适去看医生,化验没查出病,你不会说化验"成功"——钱花了、病还在。化验查出溃疡,才叫成功,因为可以开始治疗。开始测试时,就把程序当成那个"病人"。
  • 填字游戏:如果让你 15 分钟做完《纽约时报》周日版填字游戏,你大概 10 分钟就放弃了(任务不可能);给你 4 小时,前 10 分钟反而更有进展。把测试定义为"找错",它才是一个够得着的目标。

还有第三种错误定义的问题:程序"做了它不该做的事",同样是错。哪怕三角形程序能正确区分所有有效三角形,只要它把 1,2,3 判成不等边、或把 0,0,0 判成等边,它依然有错。所以原则 6 说:看程序"有没有做该做的事"只是一半,另一半是看它"有没有做不该做的事"。

为什么"穷举"永远行不通

黑盒:穷举输入测试

若把程序当黑盒(不看内部,只按规格造输入),要"测全"就得穷举所有输入——不可行:

  • 三角形程序:就算你用 3,4,5 是对的,也无法排除程序里藏着 if (a==3842 && b==3842 && c==3842) 判成不等边 这种特判。要确保发现它,只能试遍所有可能的输入(有效+无效),实际上是无限多个。
  • C++ 编译器:既要造出所有合法的 C++ 程序,还要造出所有非法的 C++ 程序来验证它都会拒绝——两个都是无限集。
  • 订票系统:事务的执行依赖之前的事务状态,于是不仅要试所有合法/非法事务,还要试事务的所有可能序列。

白盒:穷举路径测试

若看代码逻辑(白盒),"测全"的对应目标是执行程序的每条路径——同样不可行:

书里一个只有 10–20 行、含一个最多迭代 20 次的 DO 循环、循环体里嵌套 IF 的小程序,其独立路径数约为 10¹⁴(100 万亿)。换算一下:每 5 分钟写完并执行一个用例,要跑约 10 亿年;就算快 300 倍(每秒一个),也要 320 万年。

而且"跑遍所有路径"还有三个根本缺陷:

  1. 不保证符合规格:要写升序排序却写成降序,路径全跑过也没用——程序是"错的那个程序"。
  2. 发现不了"缺失的路径":少写了一条必要分支,路径测试测不出来。
  3. 发现不了数据敏感性错误:例如本意是 if (abs(a-b) < c),却写成 if (a-b < c)——只在特定 a、b 取值下才出错,走遍所有语句/路径也未必命中。

结论:你不可能测试出一个"绝对无错"的程序;因此测试的本质是一个经济学问题——用有限数量的测试用例,最大化错误检出率(yield)。

10 条测试准则(第 2 章,精炼版 + 案例)

  1. 测试用例必须定义预期输出——否则"人眼只看见想看见的",会把貌似合理的错结果当对结果。
  2. 程序员应避免测自己的程序——就像作者不该自己校对:知道自己想说什么,就看不见实际写了什么;且从"建设"切到"破坏"心态极难。(注:调试——修正已知错误——反而该原作者来。)
  3. 开发组织不该测自己的程序——进度与成本 KPI 会损害客观性。
  4. 必须仔细检查每次测试的结果——实验反复显示:症状明摆在输出里,仍被漏看。
  5. 用例要覆盖无效、意外的输入,不只有效输入——多数人只测"正常路径";比如没人拿 1,2,5 去试三角形程序。
  6. 一半看"没做该做的",一半看"做了不该做的"——例如工资程序:就算每张支票都对,只要它多开了不存在员工的支票、或覆盖了人事文件的第一条记录,它仍然有错。
  7. 别用"用完就扔"的用例——除非程序真是一次性的。随手在终端上临时编用例,改完代码要重测时又得重编,人们往往偷懒,于是回归测试形同虚设,改坏旧功能也不自知。→ 这就是回归测试的由来。
  8. 别在"不会发现错误"的假设下规划测试——那等于默认"测试是证明程序正确",方向就错了。
  9. 错误成簇:某段程序已发现的错误越多,该段还有更多错误的概率越大。所以发现 bug 后,在这一带继续挖,投资回报最高。
  10. 测试是极具创造性的智力活动——难度可能超过设计程序本身。

人工测试:检查、走查(第 3 章)

三种人工方法:代码检查(inspection)、走查(walkthrough)、用户(可用性)测试。前两者是代码导向的。

  • 谁来:由作者以外的人组成小组(3–4 人最佳,只有 1 人是作者)——呼应原则 2。它比老的"桌面检查(自己读自己的代码)"更有效。
  • 干什么:目标是"会议式地找错",但不解决错——即"测试,而非调试"。
  • 好处:错误通常被精确定位到代码(不像黑盒只看到"结果异常");而且常一次暴露一批错误,可以集中修。电脑测试通常只暴露症状(不终止、打印垃圾值),错误得一个个找。
  • 效果:能找出典型程序中 30%–70% 的逻辑设计与编码错误;但查不出高层设计错误(如需求分析阶段的错)。注意:这 70% 不是"全部错误的 70%",而是"最终发现的所有错误中的 70%"。
  • 错误检查单(error checklist) 是检查流程的重要部分,分 6 类:
    1. 数据引用错误(Data Reference)——最典型是变量未初始化就使用;数组下标越界、下标非整数、悬挂引用(dangling reference)、别名类型错配(Fortran EQUIVALENCE、COBOL REDEFINES)、off-by-one 等。
    2. 数据声明错误(Data Declaration)
    3. 计算错误(Computation)
    4. 比较错误(Comparison)
    5. 接口错误(Interface)
    6. 输入/输出错误(Input/Output)
  • 作者吐槽:很多检查单只盯着代码风格(对齐、注释是否漂亮),而不是真正的错误——那样没用。

测试用例设计:黑盒四法(第 4 章)

1)等价类划分(Equivalence Partitioning)

把每个输入条件分成有效等价类和无效等价类,每类取一个代表值(并按原则 5,重点覆盖无效类)。常见规则:

  • 输入条件是范围(如 -1.0 ~ 1.0)→ 1 个有效类 + 2 个无效类;
  • 是具体值/集合 → 1 个有效类 + 1 个无效类;
  • 是布尔→ 1 有效 + 1 无效。

例:三角形"能构成三角形"可分成"两边之和大于第三边"(有效)与"不大于"(无效)两类,3-4-5 和 1-2-4 可作代表。

2)边界值分析(Boundary Value Analysis,BVA)

经验表明:边界条件的测试收益最高。它比等价类更强——不取代表值,而是专攻等价类边界上、略上、略下的点,并且同时考虑输入空间和输出空间。书里的规则:

  • 范围 → 取端点,再取"刚越过端点"的无效值。例:合法域 -1.0 ~ 1.0 → 测 -1.0, 1.0, -1.001, 1.001。
  • 个数 → 取最小、最大,及再多一、再少一。例:文件可含 1~255 条记录 → 测 0, 1, 255, 256。
  • 对每个输出条件也照做。例:FICA 扣款范围 $0.00 ~ $1165.25 → 造出 $0.00 和 $1165.25,并试造负扣款或超额扣款。
  • 有序集合 → 关注首尾元素。

标志性案例:为什么 BVA 比等价类强?若把 A+B>C 误写成 A+B>=C,用等价类的 3-4-5 和 1-2-4 都测不出来;但边界用例 1-2-3 立刻暴露它会把非三角形判成三角形。

大案例——MTEST 阅卷程序:输入是 80 字符的定长记录文件(标题记录、正确答案记录、学生答案记录;题目数 1–999,学生数最多 200),输出 4 种报告。作者逐条扫规格,把边界用例推到了 42 个,例如:

  • 空输入文件、缺标题记录、1 字符标题 / 80 字符标题;
  • 题目数 0 / 1 / 50 / 51 / 999(注意:50→51 是"需要第二条答案记录"的边界)、非数字值;
  • 答案记录"多一条 / 少一条";
  • 学生数 0 / 1 / 200 / 201;
  • 全部同分 / 全不同分 / 部分同分(验排名);某学生得 0 分 / 满分;
  • 标准差取最大(一人 0 一人 100)/ 取 0(全部同分);
  • 报告刚好排满一页 / 差一个排满……

作者点题:这 42 个用例几乎都是程序员真的会犯的错,但随机或即兴测试根本碰不到它们。

3)因果图(Cause-Effect Graphing)

等价类和 BVA 的短板:不探索输入条件的组合(组合数会爆炸)。因果图把规格翻译成布尔逻辑图,系统化地选出高收益组合,附带好处是暴露规格里的不完备与歧义。步骤:

  1. 把规格拆成可处理的小块;
  2. 识别因(cause,输入条件)与果(effect,输出条件 / 系统状态变化),编号;
  3. 转成布尔图(与 / 或 / 非);
  4. 加约束(哪些因/果组合不可能);
  5. 追踪状态,转成受限判定表;
  6. 判定表每列 → 一个测试用例。

小案例:规格 =「第 1 列必须是 A 或 B;第 2 列必须是数字;满足则更新文件;第 1 列错则发消息 X12;第 2 列错则发消息 X13」。
- 因:1=列1是A、2=列1是B、3=列2是数字;果:70=更新、71=X12、72=X13。
- 图里还发现一个不可能组合:因 1 和因 2 不能同时为真(一个字符不可能既是 A 又是 B)——映射到约束。

4)猜错法(Error Guessing)

凭直觉与经验枚举"易错情形",无固定流程。书里的例子:

  • 测排序子程序:空列表、只有一个元素、所有元素相同、列表已排好序。
  • 测二分查找:表只有 1 个元素;表长为 2 的幂(如 16);表长为幂次±1(如 15 或 17)。
  • 测 MTEST:程序接受"空白"答案吗?学生的名字/学号有重名吗?奇数个和偶数个学生(中位数算法不同)?题目数字段为负值?

测试用例设计:白盒覆盖阶梯(第 4 章)

白盒关心"用例覆盖了多少逻辑",由弱到强(书中示例程序 foo(A,B,X):if(A>1 && B==0) X=X/A; 与 if(A==2 || X>1) X=X+1;):

准则 要求 弱点
语句覆盖 每句至少执行一次 太弱、基本无用:A=2,B=0,X=3 走路径 ace 就"全覆盖",却漏掉 and 该是 or、X>1 该是 X>0、以及 X 未被改动的路径 abd
判定 / 分支覆盖 每个判定的真、假各一次 仍弱:X 是否被改动,只有 50% 概率碰到
条件覆盖 判定内每个条件的两种结果各一次 不一定覆盖判定结果:A真B假+A假B真 覆盖了条件,却没让 IF(A && B) 的 THEN 执行
判定 / 条件覆盖 上面两者都满足 会遇到短路"遮蔽":and 为假时后续条件不再求值,逻辑错可能仍被漏掉
多重条件覆盖 判定内所有条件结果组合都覆盖 最强;foo 需要覆盖 8 种组合(可用 4 个用例,因 4 个组合每组可配对)

推荐策略:先用黑盒方法设计用例,再用白盒方法补充必要的用例——各法盲区不同,互补才严谨。

案例:模块测试与更高层次(第 5、6 章)

  • 模块(单元)测试:先对模块逻辑用白盒方法,再用黑盒方法补充其规格测试。书中以 BONUS 模块为例:给销售额最高部门的员工加薪 $2000,但若现薪 ≥ $150,000 或是经理,则只加 $1000;表为空返回错误码 1,找不到合格员工返回 2。
  • 更高层次测试(第 6 章):功能测试 → 系统测试 → 验收测试,另含 α / β 测试、配置/安装测试、回归测试,以及性能 / 压力 / 恢复 / 安全等专项。
  • 其余章节:第 7 章 可用性(用户)测试、第 8 章 调试、第 9 章 敏捷环境下的测试、第 10 章 互联网应用测试、第 11 章 移动应用测试,附录为"极限测试样例"。

落到你的科研 / ML 代码上

  • 先写预期输出再跑(原则 1 / 第 14 问):测数据管线、后处理、指标代码时,手工算一组小样例的期望结果再对照——能抓出大量"看着合理其实错了"的静默 bug。
  • 专攻边界(BVA):张量形状、索引、空/单样本、channel 顺序、归一化边界,都是 CV 代码的重灾区,用"刚沾边 / 略上 / 略下"的思维逐个过。
  • 覆盖无效输入(原则 5):别只喂正常样本——空 batch、全同类别、极端长宽比、损坏文件,往往才是崩溃点。
  • 错误成簇(原则 9):在一处挖到 bug,就在附近继续挖。
  • 回归测试(原则 7):把测试用例留成脚本,改完代码重跑——科研代码最常见的坑就是"改了 A、弄坏了 B 而不自知"。
  • 检查单思维(第 3 章):代码评审时重点盯未初始化变量、越界下标、off-by-one、接口类型错配这几类高频错。

一句话:别把"跑通、指标看着还行"当成测试通过。 测试的目标是找错,不是求心安。

术语表 / Key Terms

  • software testing — 软件测试;本书定义:以发现错误为目的而执行程序的过程
  • debugging — 调试;定位并修正已知错误(与"测试=找错"分工不同,调试反而适合原作者来做)
  • destructive / constructive — 破坏性 / 建设性(心态);测试要"破坏",开发要"建设"
  • black-box testing — 黑盒(数据驱动 / 输入输出驱动)测试;只看规格、不看内部
  • white-box testing — 白盒(逻辑驱动)测试;基于源码逻辑
  • test case — 测试用例;= 输入数据 + 该输入的预期输出
  • exhaustive input testing — 穷举输入测试(不可行)
  • exhaustive path testing — 穷举路径测试(不可行)
  • equivalence partitioning — 等价类划分
  • valid / invalid equivalence class — 有效 / 无效等价类
  • boundary value analysis (BVA) — 边界值分析;取等价类边界的"刚沾边 / 略上 / 略下"
  • cause-effect graphing — 因果图(法);规格→布尔逻辑图→判定表
  • decision table — 判定表;每列是一个测试用例
  • error guessing — 猜错法;凭直觉/经验枚举易错情形
  • statement coverage — 语句覆盖(最弱)
  • decision / branch coverage — 判定 / 分支覆盖
  • condition coverage — 条件覆盖
  • decision/condition coverage — 判定/条件覆盖
  • multiple-condition coverage — 多重条件覆盖(最强逻辑覆盖)
  • masking / short-circuit — (条件)遮蔽 / 短路求值;and/or 提前终止导致逻辑错被掩盖
  • data-sensitivity error — 数据敏感错误;只在特定取值下才暴露(如 a-b<c)
  • code inspection — 代码检查;正式的同行评审
  • walkthrough — 走查;(非正式)代码评审,3–4 人
  • deskchecking — 桌面检查;作者自查,效果差
  • error checklist — 错误检查单;6 类:数据引用 / 数据声明 / 计算 / 比较 / 接口 / 输入输出
  • dangling reference — 悬挂引用;指针寿命长于其指向内存的寿命
  • off-by-one — 差一错误(边界索引)
  • module (unit) testing — 模块(单元)测试
  • higher-order testing — 高层次测试(功能 / 系统 / 验收等)
  • function / system / acceptance testing — 功能 / 系统 / 验收测试
  • alpha / beta testing — α / β 测试
  • regression testing — 回归测试;改代码后重跑旧用例,防"改坏旧功能"
  • stress testing — 压力测试
  • error cluster / error-prone — 错误聚集 / 易错(区域);见原则 9
  • throwaway test case — 一次性(用完即弃的)测试用例
  • test-case yield — 测试用例(对错误的)检出率
  • side effect — 副作用;程序做了它不该做的事(原则 6)
  • root cause — 根因(调试时定位的那个"真正的错")

论文/书籍笔记系列。相关:软件构架:本质任务和附属任务(Brooks);软件构架:按什么标准把系统切成模块(Parnas);科研软件开发的软件工程实践(Arvanitou 等)。

posted @ 2026-09-12 18:10  Quentin_Yws154  阅读(29)  评论(0)    收藏  举报