为什么一个开发者要看测试书

《Full Stack Testing》这本书不厚,但覆盖了从单元测试到探索性测试的整套方法论。以下不是书的内容摘要,是我读完后觉得真正有用、并且从开发者视角重新理解过的部分。


测试不是找 bug

这是书里让我重新校准的第一个概念。

找 bug 是测试的结果,不是测试的目的。测试的目的是提供信息——这段代码在什么条件下正常工作,在什么条件下会出错,错了之后的表现是否可接受。这个信息服务于一个决策:现在能上线吗?

把测试理解为”信息获取”而不是”质量检验”,会改变你做测试的方式。你不会问”这个功能有 bug 吗”,而是问”在所有我知道的边界条件下,这个功能的行为我都看清楚了吗”。前者只有一个答案(有/没有),后者会引导你系统地列出一组场景。


等价类划分与边界值分析

这两个是测试理论里的基础,几乎是所有测试方法论的起点。

等价类

假设有一个输入值 x,业务逻辑分三段处理:x < 500、500 ≤ x ≤ 1500、x > 1500。理论上三个区间里的任意值行为都应该一致。所以你不必测全部数值,只需要在每个区间挑一个代表。

等价类的核心思想:把无穷的输入空间压缩到有限个”行为相同的组”里

从开发者视角看,等价类划分就是问自己一个问题:**”这段代码有多少条不同的执行路径?”** 每个 if 分支、每个 switch case、每个返回值不同的边界,都是一个等价类。

边界值

等价类的”边界”是最容易出 bug 的地方。原因很简单:开发者在写 if x < 500if x >= 500 的时候,容易把等于号放错位置。

所以边界值分析就是:在每个等价类的边缘,多测一个值。

1
2
等价类:[0, 500)  [500, 1500]  (1500, ∞)
边界值:0, 1, 500, 501, 1500, 1501

<=< 的区别,在代码里是一个字符。在测试里要精确到两个相邻的整数。

读到这里的时候我在想我写的那些代码——边界判断几乎都是拍脑袋定的。”多少算大?500 吧。” 没想过为什么是 500,更没想过 500 这个边界本身的行为是否正确。等价类和边界值给了这个”拍脑袋”一个检查清单。


状态转换:把”历史决定行为”画出来

有些 bug 不是单次操作触发的,而是操作序列触发的。比如连续输错三次密码后账号锁定——这个 bug 只在第三次失败时才出现,单独测一次登录是对的。

状态转换图就是用来描述这种”历史依赖”的:

状态转换

每个节点是一个状态,每条边是一个触发条件。测试用例就是图中的一条路径。

从开发者的角度看,状态转换图其实就是前端路由状态机或者后端工作流引擎的另一种表达。如果你的代码里已经有一个状态机(redux reducer、订单状态流转、审批流),那测试用例就是状态机的遍历路径。如果没有——你的业务逻辑可能是散在十几个 if-else 里的隐式状态机,那画一个状态转换图本身就已经是在发现设计问题了。


成对测试:不穷举也能覆盖 80%

这是全书我印象最深的一个技术点,因为它解决的是一个我直觉上认为”无解”的问题。

比如有三个条件,每个条件有两种取值:

1
2
3
A ∈ {1, 2}
B ∈ {1, 2}
C ∈ {1, 2}

全部组合:2 × 2 × 2 = 8 组

如果每个条件有三种取值:3 × 3 × 3 = 27 组

条件再多一个,指数爆炸。真实场景里,一个配置页面可能有六个下拉框,每个四五个选项——穷举不现实。

成对测试(Pairwise Testing)的洞察是:研究表明,单因子和双因子组合导致的错误占比约 80%,三因子及以上的错误只有约 20%。

所以只需要保证每两个条件之间的所有组合至少出现一次。具体做法:

  • 单因子错误(只有 A=1 是 bug)→ 至少需要 2 组:
    {A=1, B=1, C=1}{A=2, B=1, C=1}(只改 A,其他不变)

  • 双因子错误(A=1 且 B=1 同时才触发 )→ 至少需要 4 组:
    {A=1, B=1, C=1}{A=1, B=1, C=2}{A=1, B=2, C=1}{A=2, B=1, C=1}

三因子则需要全部 8 组。但你付出 4 组的测试量,覆盖了 80% 的错误类型。投入产出比极高。

这让我想到一个日常场景:API 接口的参数组合测试。一个查询接口有三个可选参数,每个参数可以传值、不传、传空——光用脑袋想”应该测哪些组合”就已经炸了。成对测试给了一个可操作的方法:先用成对测试工具生成最小覆盖集,再补几个高频场景。比拍脑袋穷举高效得多,也比”随便测几个”有依据。


因果图和决策表

因果图和决策表是一种互补工具——决策表只管”某个条件下某个结果是 true 还是 false”,而因果图更进一步,能表达条件之间的因果联系以及哪些条件组合永远不会同时出现。

因果图

不过说实话,我读完这部分的感觉是:这两个工具更适合需求阶段用,而不是测试阶段。如果需求文档本身就已经有逻辑漏洞,因果图能在写代码之前暴露矛盾。但如果你已经对着已有代码去画因果图,大概率会发现”这里逻辑本来就不太对”但已经改不了了。它属于”事前设计好过事后补救”的范畴。


错误清单:前人的常见踩坑

书里有一组经验性的常见错误列表。这些东西不是方法论推导出来的,是”测多了,反复踩的坑”。我把它抄在这里,因为每一个都可能在某一天被自己碰上:

  • 缺少对空值和无效输入的校验,且错误提示不清晰
  • HTTP 状态码不明确——业务错误、技术错误、数据校验错误全返回 500
  • 未处理的边界条件:特定数据类型、特定状态下的极端值
  • UI 端未处理的技术异常——服务器挂了,前端白屏而不是显示”暂时不可用”
  • 页面切换、数据刷新时的 UI 闪烁或残留
  • SQL 中 LIKE= 混用,两个操作符的行为完全不同
  • 缓存未清理、会话超时未定义
  • 从不同操作系统上传文件时缺少格式校验
  • 用户点击浏览器后退按钮后表单重复提交

其中第三点——“UI 端未处理的技术异常”——是我最常在自己代码里看到的问题。try-catch 里永远只有一个 console.error(err),用户看到的要么是白屏,要么是”程序异常”四个字。

做一个开发者,看到测试人员列出这些东西,感受是很复杂的。一方面觉得”这些确实是我没想到的”,另一方面意识到:测试思维和开发思维确实是两种不同的脑回路。开发想的是”怎么让功能跑通”,测试想的是”怎么让功能坏掉”。这两种思维在同一个人脑子里很难同时存在——所以才需要独立的测试角色。


手动探索测试

书里把测试分为两类:脚本化测试(跟着测试用例走)和探索性测试(自由探索)。探索性测试强调把三个视角同时打开:

  1. 业务视角:这个功能在真实业务场景里怎么用?
  2. 技术视角:实现细节上哪里最脆弱?
  3. 用户视角:一个不了解系统的用户会发现什么问题?

探索性测试不是”随便点点”。它有一套自己的策略框架:从用户角色入手理解不同权限下的行为差异,从业务领域入手理解行业特有的工作流,从基础设施配置入手发现环境相关的问题。

我去看了看自己的开发习惯——写完一个功能后,我测试的方式就是”手动探索”。没有测试用例,没有边界值列表,就是按照”我觉得用户可能会怎么用”去点。区别在于:书里的探索性测试是有框架的探索,有 checklist 和策略;我的探索是凭直觉的探索,稳定性取决于当天心情。


测试数据与环境

书里讲了一个我从未想过的问题:测试数据的卫生

当你在同一个测试环境上反复跑测试时,上一次测试留下的数据可能污染下一次的测试结果。比如你测试”新用户注册”流程,但数据库里已经有前一天注册的测试用户,你可能测不到”首次注册成功”那条逻辑。

书里的建议:每次开始新的用户故事就部署新的构建(清空旧数据),或者为每个测试故事创建一组全新的测试数据,而不是复用已有的。

还有测试环境的问题。书里提到自治团队的概念——测试人员应该能直接访问应用日志、修改环境配置、创建测试数据,而不需要向 DevOps 团队提工单。等待外部团队响应在探索性测试期间特别致命——你想验证一个假设,但得等半天才能拿到权限。

这些内容对我来说比测试方法论本身更有冲击力,因为**它们讲的不是”怎么测”,而是”测不好常常不是因为不会测,是因为环境和流程阻碍了测试”**。


结语

我是一个开发者,不是测试工程师。读完这本书不会让我变成”会测试的人”——我依然不会在写代码之前先写测试用例。但书里的东西改变了我的一个习惯:

写完功能后,不是”跑一遍,没崩,push”,而是”按照等价类每个区间挑一个值跑一遍,边界值跑一遍,状态转换的关键路径跑一遍,再把错误清单过一遍”。

这大概多花 15 分钟。但这 15 分钟里发现的 bug,如果在生产环境被发现,修复成本至少是一个小时起——定位、复现、修、部署、回滚、道歉。

测试不是质量的保证,是信息获取的成本前置。


下篇预告:混沌工程随记的展开——从一个真实的注入实验开始写。