编码前的边界情况检查清单
边界情况不提前写,开发阶段一定会靠默认假设补齐。
快速结论
至少检查空值、重复请求、数量上限、权限差异、并发冲突、外部依赖失败和回滚成本。把这些项写入规格后,很多线上事故会在评审时就消失。
你应该先看什么
- 空值、缺省值和未知值是否有明确策略。
- 重复操作和重试是否保持幂等。
- 并发写入、限流和权限不足时系统如何表现。
可直接复用的示例
边界清单 - Null / Empty / Unknown - Duplicate / Retry / Reorder - Permission / Visibility - State Transition / Concurrent Write - Expiry / DST / Time Zone - Timeout / Partial Failure / Rollback
怎样写出 QA 能直接执行的边界条件
一条边界条件可测试,需要三件事都具体:起始状态和输入、触发动作、可观察的预期结果。只要其中一项是没有宾语的动词,或者拿形容词充当数字,它就还不是测试用例。判断方法很简单:QA 不问任何人,能不能直接写出这个测试?"合理处理边界情况"这种句子第一关就过不去。
不管规格用 Given/When/Then 还是 fixture 列表,每条边界条件都写同样几栏:
边界条件:未付账单上连点两次 Pay Now
- 起始状态:用户有一张未付账单,已保存的银行卡已过期
- 输入: 60 秒内用同一个 invoice_id 重试支付
- 动作: 用户连续点击两次 Pay Now
- 预期: 只向支付服务发起一次扣款;第二次请求
返回已有的 attempt_id
- 证据: 账本里只有一条 pending 支付记录;页面只显示一条横幅
- Fixture: duplicate-pay-now.json
给 fixture 起名字比看起来更重要。fixture 进入测试仓库后,API、UI、日志和数据库断言都能复用同一份输入,边界条件就不再只是文档里的一句话。
同样的写法,能把一句模糊的话改成 QA 不用开会就能执行的用例:
修改前: - 网络失败时要优雅处理。 修改后: - Given 已登录买家的购物车里有一个 49 美元商品 And 支付服务已经扣款,但应用收到 30 秒 timeout When 买家在 2 分钟内再次点击 Pay Then 应用复用原来的 idempotency key And 页面显示"支付处理中",不会再次扣款 And 客服能通过 order_id 在审计日志里查到这笔 pending 支付
两条结构规则让清单保持可执行。第一,写行为,不写实现:"后端重试三次"留着,"后端用指数退避加 Bloom filter 缓存"删掉。第二,每条边界条件都落到测试者能观察的结果上;"记一条 warning 日志"这类内部行为,只有规格明确把这条日志指定为观察点时才算数。
边界条件放在正常路径验收标准下面的独立小节,按本页的类别分组,格式和正常路径保持一致。具体写法见验收标准示例与写法。
输入边界:每个字段试六个值
每个输入最多有六个值得测的取值:空值、一个、允许的最大值、最大值加一、负数、Unicode 或特殊字符。不是每个字段都六个全适用,但在规格说明之前,默认它们都可能出问题。以上限 50 个字符的显示名为例:
- Given 显示名字段(最多 50 个字符) When 用户提交 ""(空值) Then 表单内联提示"名称不能为空",提交按钮禁用 - Given 显示名字段 When 用户提交 51 个字符 Then 请求被拒绝,返回 422 和"名称过长(最多 50 个字符)" - Given 显示名字段 When 用户提交包含 emoji(4 字节 Unicode)的 50 个字符 Then 请求成功,名称在用户列表中正确显示
emoji 这一条最容易被漏掉。客户端、API 和数据库可能分别按码点、UTF-16 单元或字节计算长度,规格要写明上限按哪种计数。下面高风险领域示例里的文件大小用例,用的也是"恰好等于上限"和"上限加一"这一模式。
状态转换、并发写入与时间边界
错误状态下的转换
只要有 status 字段,就有可能走错的转换。规格通常只写正常路径(active → cancelled),跳过重新激活、重复取消、变更挂起期间取消这些情况。每个转换都要写清:在错误状态下尝试时会发生什么。"用户取消一个已经取消的账户"是真实场景,上线后很快就会出现。
并发写入
只要两个用户能操作同一条记录,功能就有并发规则,不管规格写没写。要测的是两人在 100ms 内同时提交时会发生什么。规格要回答三个问题:
- 谁赢:后写覆盖、先写胜出,还是两边都返回冲突?
- 输的一方看到什么:被静默覆盖、一条错误提示,还是合并界面?
- 输的一方页面怎么同步到新状态:立即刷新、轮询,还是要手动刷新?
规格没写明"后写覆盖并显示冲突横幅"是规则,QA 就没法测它。
时间相关的边界
时间边界在时钟跨过那个点之前都看不见:
- 超时。外部调用花了 30 秒而不是 300 毫秒,用户看到的是错误、重试,还是一直转圈?
- 过期。session、token 或试用期在操作中途过期,和操作开始前就过期是两种情况。
- 夏令时。排在重复或被跳过那一小时里的定时任务,回拨时会不会跑两次,拨快时会不会一次都不跑?
- 时区。同一个日历日,UTC+13 和 UTC−12 的用户,"当天结束"分别是什么时候?
每条正常路径至少三条错误路径
每条正常路径的验收标准,至少对应三条错误路径:校验失败、权限不足、下游依赖失败。规格要写出每种情况下用户看到什么,包括状态码和确切文案:
- Given 用户已登录但没有 "admin" 角色
When 请求 /admin/users
Then 返回 403,body 为 {"error": "forbidden", "required_role": "admin"}
And 页面显示"你没有访问此页面的权限",并提供返回链接
- Given 下游计费服务返回 503
When 用户提交结账
Then 页面显示"支付暂时不可用,请一分钟后重试"
And 服务端以指数退避最多重试 3 次
And 至少一次重试成功之前不创建订单记录
失败要写成场景,不要只写名词。"上传中断"没法测;"用户在慢网络下上传 200 MB 文件,进度到 80% 时断网,随后刷新浏览器"就可以,而且它逼着产品做选择:断点续传、明确失败并允许重试,还是明确丢失进度。三种都可测,前提是规格选定其中一种。
高风险领域示例
以下示例涵盖三个边界情况代价最高的领域:支付处理、身份认证和文件摄入。在这些场景中,晚发现边界情况的修复成本极高。
# 支付处理边界情况 - amount = 0.00 → 校验错误拒绝,不调用支付网关 - 购物车与银行卡币种不匹配 → 400,不扣款 - 扣款发起后网关超时 → 记录模糊状态,触发对账任务,返回 503 含 retryable 标记 - 10 秒内重复支付请求 → 返回原始收据,不二次扣款 # 身份认证边界情况 - token 过期后 1ms 使用 → 401,非 403 - 有效 token 但账号已停用 → 403 含 account_suspended 错误码 - 同时从 3 个会话登录 → 最早会话失效,新会话签发,审计日志记录全部三个事件 - 重置密码链接首次点击后再次使用 → 410 Gone # 文件上传边界情况 - 文件大小恰好达到上限(10MB)→ 接受 - 文件大小超过上限 1 字节 → 413 含明确大小提示 - 文件类型伪造(PDF 扩展名但内容为可执行文件)→ 内容检测拒绝,非扩展名检测 - 上传中途中断 → 丢弃不完整文件,不创建记录
支付边界情况到达生产通常导致双重扣款或未对账交易——两者修复成本均高。认证边界情况未文档化会产生不一致的安全行为,在审计前不可见。文件边界情况是内容检测缺失时最常见的安全漏洞来源。
优先级排序:风险 × 可发现性
并非每个边界情况都需要相同程度的规格细节。实用的优先级排序使用两个维度:出错后的后果(风险),以及 QA 或监控在生产中发现它的难易程度(可发现性)。高风险叠加低可发现性,要求在实现前明确写入规格并映射测试用例。
- 高风险 + 低可发现性——编码前必须文档化并测试:支付冲突、并发写入、权限边界失败、数据损坏路径。
- 高风险 + 高可发现性——文档化并添加监控:性能下降、第三方中断、schema 校验失败。
- 低风险 + 低可发现性——文档化为非目标并说明理由:非默认语言的渲染边界情况。
- 低风险 + 高可发现性——记录但不阻塞:有明确用户提示的次要校验错误。
这个优先级排序的输出不是所有可能边界情况的完整列表——那既不可能也适得其反。而是团队在编码前达成共识的优先列表,每项均标明测试责任人和解决时限。
哪些边界条件会阻断发布
风险 × 可发现性决定一条边界条件要在规格里写多细。还有一个决定也应该写进规格:它的测试失败时怎么办。提前写好规则,QA 就不用在执行测试时临场谈优先级。
边界条件优先级规则 阻断发布: - 资金流转可能重复或丢失 - 权限边界可能被绕过 - 用户数据可能在没有提示的情况下被覆盖 - 重试可能产生第二次不可逆操作 - 客服无法识别失败状态 可作为已记录风险上线: - 文案与界面不一致的外观问题 - 罕见且不丢数据的时序问题 - 非关键通知延迟,且有明确重试路径 - 仅管理员使用、有人工兜底方案的流程
需要中间档的团队,可以把其余边界分成 release-blocking、should-test 和 exploratory 三级。叫法不重要,重要的是测试一失败,团队马上知道该停止发布、接受风险,还是放进下个迭代。
跨系统层的边界情况
边界情况在不同系统层的分布规律各异。一个用户操作可能同时在 API 层、服务层、数据库层和后台任务层暴露各自的边界情况。规格应标明每个边界情况所在的层次,以确保正确的工程师负责对应的测试。
- API 层:请求体格式错误、缺失必要请求头、超大 payload、意外的 Content-Type。
- 服务层:依赖不可用、熔断器状态、重试风暴行为、缓存失效时机。
- 数据库层:唯一约束冲突、删除时外键失败、事务隔离级别影响、迁移回滚对现有数据的冲击。
- 后台任务层:负载高峰下的任务队列积压、确认失败导致的重复执行、毒消息处理、死信队列行为。
任务层的边界情况是最常见的覆盖盲区。由于任务异步运行,其失败对用户在请求时不可见——这正是它们需要明确规格覆盖的原因。一个静默丢弃记录的未文档化任务失败模式,比用户可以重试的同步错误危害更大。
真实评审矩阵:大报表导出
边界清单要落到具体功能里才有用。下面是一个大报表导出的评审矩阵,它把“用户点导出”拆成 UI、API、后台任务和存储四层。
功能:订单报表 CSV 导出 UI 层: - 用户连续点击两次导出 - 期望:返回同一个 job_id,或第二次点击被禁用 - 证据:重复点击测试、按钮 loading 状态截图 API 层: - 请求超过最大行数 - 期望:返回 413 或清晰校验错误,不创建 job - 证据:row_limit fixture、错误文案检查 Job 层: - worker 在生成一半时退出 - 期望:重试或标记 failed,并清理半成品文件 - 证据:worker failure test、partial file cleanup log Storage 层: - 下载链接过期 - 期望:显示过期状态,并允许有权限用户重新生成 - 证据:expired URL test、权限检查截图
这类矩阵比一长串泛泛的边界词更有用,因为它直接告诉评审者:哪一层负责处理,失败后用户看到什么,合并前要拿什么证据。
落地建议
拿最近一个上线后出问题的功能做回溯:这份清单里哪几项当时没覆盖?大多数时候答案是空值处理、重试幂等或并发写冲突。找到了就在下一个功能的规格里提前写进去。
如果团队觉得边界情况太多,可以先只记录“高风险且低可发现性”的项。这样不会让规格膨胀,同时能优先覆盖最可能造成生产损失的路径。
评审时也可以给每个边界情况标记处理方式:立即实现、写成非目标、加监控观察,或放入后续版本。标记后的清单更容易进入测试计划和发布检查,也能避免团队把所有边界情况都当成同等优先级。
相关文章
评审时如何使用这份清单
建议在实现前把清单拆给不同角色:产品检查空状态和用户期望,研发检查并发、幂等和数据边界,QA 检查每个边界是否有可执行用例,运维或值班负责人检查异常是否可观测、可告警、可回滚。
如果某个边界暂时不处理,也要写清理由和风险接受人。未处理的边界不一定阻止上线,但它应该是一个明确决策,而不是评审时漏看的空白。
QA 什么时候应该把规格退回
看到下面这些情况,QA 完全有理由把规格退回去修改:
- 边界条件写成散文而不是验收标准。"优雅处理过期会话"是愿望,不是测试。
- 错误路径没有状态码或确切文案。"提示一个错误"没法断言。
- 整类缺失,比如有共享状态的功能里看不到并发这一节。
- 用形容词代替数字:"快""合理""明显"都没法断言。
- 测试准备没写清:数据、权限、功能开关、时间、网络状态和已有记录。
规格定稿前跑一遍"五分钟测试":挑一条边界条件,假装自己是 QA 来写测试。不打开代码、不问人,就能写出确切的输入、触发和断言,这条就过了;写不出来,规格还欠一句话。仍需产品拍板的边界条件,退回规格里写成开放问题,别让 QA 在执行测试时才发现这个决定没人做。
把边界条件接回测试和发布
边界条件只有进入测试和发布判断,才算真正完成。评审结束后,应把高风险项分别落到自动化测试、手工验证、监控指标或发布观察清单里。无法验证的边界条件,需要改写成可观察状态,例如错误码、空状态文案、审计日志或告警阈值。
如果某个边界条件暂时不处理,也要写进非目标或后续任务。这样上线后出现相关问题时,团队能判断这是有意延期,还是评审时遗漏。
编辑说明
本指南涵盖 编码前的边界情况检查清单,面向 Spec-First 工程团队。示例为说明性场景,非生产代码。
- 作者详情: Spec Coding 编辑部
- 编辑政策: 我们如何审核和更新文章
- 纠正: 联系编辑