从验收例子到自动化测试:怎样知道没有改坏
昨天收藏还能用,今天改完列表排序,取消收藏突然失效。你不可能每改一行都手动检查整个网站。自动化测试让程序重复执行一些检查,帮助尽早发现变化;但先要知道该检查什么,而不是先追求一个很高的覆盖率数字。
1. 从用户承诺选检查点
回到需求卡:重复保存不增加记录;取消以后列表不再包含文章;不同用户互不影响。每一条都能变成一个测试。先写预期,再选择如何操作,不要照着实现代码逐行抄一份“测试”。
例如代码错误地保存两次,测试却只检查 save 被调用了,就可能照样通过。我们更需要检查最终条数和归属。调用次数有时确实重要,比如防止重复收费,但应由明确行为决定。
2. 先运行一个最小测试
本课程仓库提供独立示例 examples/software-engineering,使用 Node.js 24 的内置测试工具,不需安装额外依赖。下面是核心函数,输入为当前列表、文章 ID 和期望状态,返回新列表:
export function setFavorite(ids, articleId, saved) {
const next = new Set(ids)
if (saved) next.add(articleId)
else next.delete(articleId)
return [...next]
}
Set 保存不重复的值。函数先复制输入,因此不会直接修改原数组;请求“设为已收藏”两次仍得到同样集合。这里演示本地状态规则,不包含身份、数据库或并发写入,不能拿它证明线上保存安全。
对应的最小测试如下,保存为示例目录里的测试文件时使用同目录模块路径:
import test from 'node:test'
import assert from 'node:assert/strict'
import { setFavorite } from './favorites.mjs'
test('重复保存同一文章仍只有一条', () => {
const once = setFavorite([], 'article-1', true)
const twice = setFavorite(once, 'article-1', true)
assert.deepEqual(twice, ['article-1'])
})
先准备输入,再执行动作,最后比较实际与预期。这是很多测试共同的结构。完整示例还补充取消、输入不变以及两个独立列表的检查。
3. 在不同层次发现不同错误
单元测试检查小范围规则,如上面的函数。集成测试检查部件合作,例如用真实测试数据库验证唯一约束和事务。端到端测试从用户入口操作,例如登录、收藏、刷新、查看列表。
这些名称的具体边界会因团队而异,关键是说明测试包含哪些真实依赖。把数据库全部换成返回成功的假对象,就没有验证真实数据库能否拒绝重复记录。反过来,每条字符串处理规则都通过浏览器检查,会让反馈更慢、更难定位。
常见的测试金字塔建议多做快速、范围小的检查,辅以较少的跨系统测试。这是成本与反馈的思路,不是所有项目都必须遵守的固定数量比例。
4. 正常、边界、失败都要选代表
对于收藏上限,检查零条、刚好到上限、超过上限;对于标题输入,检查空字符串、合法长度和超过限制;对于保存,检查权限拒绝和请求失败。按输入类别选代表例子,常比随机写十个相似成功用例更有效。
时间、网络和随机数会让测试不稳定。需要可控时钟、固定样本或明确等待条件,而不是到处增加任意的睡眠。隔离外部依赖时要记得补真实集成检查,避免假对象与实际接口逐渐不同。
5. 覆盖率告诉你什么
覆盖率能提示哪些代码没有执行过,但不能证明断言有用,更不能证明需求完整。一个测试可能跑过每行代码,却从没检查结果。应优先看关键风险是否有证据,而不是为达到 100% 添加无意义检查。
先写失败测试、再实现、再整理代码,是测试驱动开发常见的循环。测试应因为缺失目标行为而失败,而不是因为路径写错。它是可选工作方法,不是把所有测试都写在前面的一次性流程。
练习与参考答案
给示例补一个“取消不存在的文章”测试。预期返回内容不变,也不报错,因为本例把取消定义为设置为未收藏。若产品采用另一种契约,预期也要相应调整。再故意把 Set 换成直接追加,重复保存测试应失败;这能检查测试是否真的抓住了规则。
