撰写 QA 真正能测试的边界情况
"合理处理边界情况。"我几乎每周都能在 spec 里看到这句话。它什么都没说。QA 没法测试"合理"——他们需要具体的输入、具体的触发条件、具体的预期结果。下面是我写边界情况的方式,让 QA 不用追问一句就能把它们变成测试用例。
内容整理说明
复查日期:2026-05-06。本文已重新纳入公开索引路径,作为 验收标准 Hub 的延伸阅读。我们补齐了专题路径、站内链接和可索引元数据,便于搜索引擎和读者理解它与核心主题的关系。
"可测试"的判定标准
一条边界情况是可测试的,前提是三件事都具体:输入、触发条件、以及可观察到的预期结果。如果其中任何一项是没有宾语的动词、或是拿形容词充当数字,那它就还不是测试用例——只是一厢情愿。
快速检查的方法是:QA 工程师不打开 Slack 就能写出这个测试吗?如果答案是"不能",那 spec 就还没写完。
我每次审 spec 都会对照的五个类别
绝大多数被漏掉的边界情况都落在这五个桶里。我审 spec 的时候会把这份清单开着,QA 写测试计划时也开着。
1. 输入边界
每个输入字段至少有六个值得关注的取值:空值、一个、允许的最大值、最大值加一、负数、以及 unicode 或特殊字符。不是每个字段都六个都适用,但默认假设应该是"六个都可能要命",除非你能证明不会。
- Given the display name field (max 50 chars) When the user submits "" (empty) Then the form shows "Name is required" inline; submit is disabled - Given the display name field When the user submits 51 characters Then the request is rejected with 422 and "Name too long (max 50)" - Given the display name field When the user submits 50 characters including emoji (4-byte unicode) Then the request succeeds and the name renders correctly in the user list
2. 状态转换
任何带有 status 字段的东西都有可能走错的转换路径。大多数 spec 描述的是快乐路径("user → active → cancelled"),却跳过那些乱糟糟的路径:重新激活、重复取消、在挂起变更期间取消。生产事故就住在这些地方。
对于每一个状态转换,都要写清楚:在错误状态下尝试这个转换时应该发生什么。"用户尝试取消一个已取消的账户"是真实存在的场景,而且在上线第一天就会发生。
3. 并发与竞态条件
只要两个用户可以操作同一条记录,你就已经有了并发问题——无论 spec 承不承认。真正要测的不是快乐路径能不能跑通,而是两个用户在 100ms 内都点了提交时会发生什么。
- 同时写入谁赢——后写覆盖、先写胜出、还是两边都返回冲突?
- 输的那个用户看到什么?默默被覆盖、一条错误提示、还是一个合并界面?
- UI 如何把新状态反映给输的用户——立即刷新、轮询拿到、还是要他自己刷新才会恢复?
把这些写下来。QA 没法测试"后写覆盖并显示冲突横幅",除非 spec 明确规定这就是规则。
4. 时间相关的边界
跟时间相关的边界往往咬得最狠,因为它们在时钟跨过那个点之前是看不见的。常见的几种:
- 超时——外部调用跑了 30 秒而不是 300 毫秒时会怎样?用户看到错误、看到重试,还是标签页就一直转圈?
- 过期——session、token 或 trial 过期的那一瞬间会发生什么?操作中途过期和操作前过期是两回事。
- 夏令时切换——定时任务在凌晨 1 点会触发两次,还是会跳过 2 点?
- 时区——对于同一个日历日,UTC+13 的用户和 UTC-12 的用户来说,"当天结束"是什么时候?
5. 错误路径
每一条快乐路径 AC 至少对应三条错误路径:校验失败、授权失败、下游依赖失败。spec 应当说明用户在每一种情况下看到什么。
- Given the user is authenticated but lacks "admin" role
When they request /admin/users
Then the response is 403 with body {"error": "forbidden", "required_role": "admin"}
And the UI shows "You don't have access to this page" with a link back
- Given the downstream billing service returns 503
When the user submits checkout
Then the UI shows "Payment is temporarily unavailable — please retry in a minute"
And the request is retried up to 3× with exponential backoff server-side
And no order record is created until at least one retry succeeds
如何在 spec 里组织这些内容
我会把边界情况放在快乐路径 AC 下面的一个专门小节里,按上面五类分组。每一条边界情况都有一个 Given/When/Then 块,格式跟快乐路径一致。
我强制执行两条结构规则:
- 边界情况不能描述实现。"后端重试三次"说的是行为——留着。"后端使用指数退避加 Bloom filter 缓存"说的是实现——删掉。
- 每一条边界情况都要有一个用户可观察的结果。仅发生在内部的行为(比如"记一条 warning 日志")对 QA 来说不可测,除非 spec 明确把这条日志指定为可观察项。
QA 可以把 spec 打回去的几种情况
如果我站在 QA 这边做评审,看到下面这些情况我会把 spec 退回去让作者改:
- 边界情况写成散文,而不是 AC。"优雅处理过期的会话"是愿望,不是测试。
- 错误路径没有具体的状态码或错误信息。"提示一个错误"没法断言。
- 整类整类地缺失。如果一个涉及共享状态的功能里看不到并发那一小节,那就是红灯。
- 用形容词代替数字。"快"、"合理"、"明显"——这些都没法断言。
五分钟测试
发 spec 之前跑一遍这个检查:挑一条边界情况,假装你是 QA 在写测试。你能在不打开代码、不问人的前提下,写出确切的输入、确切的触发、确切的断言吗?可以的话,这条就过了。不行的话,spec 还欠你一句话。
把这个检查扩展到整个边界情况小节,你就能在它们三周后出现在 QA 站会之前,提前抓住大约 80% 的歧义。
评审时看什么
这篇文章适合用在把边界条件交给 QA时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。
- 给出具体输入,而不是写“异常情况”。
- 说明测试前的状态和权限。
- 写出 UI、API、日志或数据库里的可见结果。
- 标明哪些边界阻塞发布,哪些可后补。
QA 不缺“多考虑边界”的提醒。QA 缺的是可以直接执行的例子。
例子:“网络失败”不是可测试边界。要写成“用户点击支付后,支付 API 超时 30 秒;订单保持 pending,禁止重复支付,并显示重试按钮”。QA 才知道怎么复现,也知道怎样算通过。
落地例子
文件上传的边界条件可以写成一个场景:用户在慢网络下上传 200 MB 文件,进度到 80% 时断网,随后刷新浏览器。预期结果可以是断点续传、明确失败并允许重试,或者明确丢失进度。哪一种都可以测试,关键是不能只写“处理中断上传”。
边界条件越像真实场景,QA 越少猜测。规格也能暴露产品选择:我们是在保护用户时间,还是接受重新上传的成本。
边界条件要带输入,不要只写名词
QA 不能测试“处理异常情况”。他们能测试的是一个具体输入、起始状态、操作和预期输出。写边界条件时,我会强迫每条都包含这四个部分。
Testable edge case: - Start state: user has one unpaid invoice and card is expired - Input: retry payment with same invoice_id within 60 seconds - Action: user clicks Pay Now twice - Expected: one provider charge attempt, second request returns existing attempt_id - Evidence: ledger has one pending payment row and UI shows one banner
边界:不要把低概率但低影响的情况全塞进首版。优先写会导致钱错、权限错、数据丢失和用户无法恢复的边界。
给边界条件标优先级
QA 时间有限,边界条件要分 release-blocking、should-test 和 exploratory。涉及钱、权限、数据删除、重复提交和不可恢复状态的边界优先级最高。排序写进 spec,测试取舍才不会靠临场感觉。
边界条件写完后,再补一列证据:API response、数据库状态、事件、日志、UI 文案或截图。QA 不该靠读代码判断通过。每条边界都能连到证据,才算从“想到了”变成“测得到”。
我还会让每个边界条件带一个 fixture 名称,例如 expired-invite.json、duplicate-refund.json、permission-revoked.json。fixture 进入测试仓库后,API、UI、日志和数据库断言都能复用同一份输入,边界就不再只是文档里的句子。
前后对比:把“边界情况”改成 QA 可测夹具
“网络失败要处理好”和“QA 可以直接测”的差别,在于是否给了夹具。第二种写法可以直接放进测试用例,不需要再开会问产品。
修改前: - 网络失败时要优雅处理。 修改后: - Given 已登录买家的购物车里有一个 49 美元商品 And 支付服务已经扣款,但应用收到 30 秒 timeout When 买家在 2 分钟内再次点击 Pay Then 应用复用原来的 idempotency key And 页面显示“支付处理中”,不会再次扣款 And 客服能通过 order_id 在审计日志里查到 pending payment。
这段不长,但它交代了数据、触发、时间窗口、断言和客服排查信号,联调和验收都更容易落地。
专题阅读路径
先读主题 Hub,再用下面的相邻文章和模板把这篇内容放进完整工作流。
继续阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
本文面向软件交付团队,介绍如何撰写 QA 真正能测试的边界情况。示例均为工程场景说明,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:文章审阅与更新方式
- 纠错:联系编辑