如何撰写可测试的软件规格文档
"可测试"的规格并不是用某种特殊语言写成的规格。它是一份每一条声明都能由未参与撰写的人来核验的规格。这个门槛听起来不高,其实不然。我审阅过的大多数规格在第一轮就过不了这关——包括我自己写的。
现场笔记:最快识别不可测试规格的方法
我会直接问一个很笨的问题:QA 要准备什么 fixture?如果没人说得出初始状态、触发动作和预期输出,那句话还不是可测试需求。
不可测试: - 仪表盘能优雅处理过期数据。 可测试: - Given 库存数据超过 15 分钟未同步 When 仪表盘加载 Then 时间戳旁显示 stale 标记 And 同步任务完成前禁用刷新
可测试性到底需要什么
当一条声明能被某人执行一次具体的检查,并得到确定的"是"或"否"时,它才算可测试。这要求四件事情被钉死:初始状态、输入或触发条件、可观察的结果,以及检查本身。少一项,声明就塌陷成了个人观点。
对比下面两句话,都是我实际审阅过的规格里摘出来的:
- "搜索功能应该快速返回相关结果。"
- "Given 查询至少匹配一个已索引文档,when 用户提交该查询,then 首页结果(10 条)在 95 分位延迟 200ms 内返回,按相关性得分降序排列。"
第一句不可测试。第二句里有四条可测试的声明:前置条件(索引命中)、触发条件(提交)、可观察项(10 条结果页、p95 延迟、相关性排序)、检查方式(计数、测量、核验顺序)。永远写第二种。
四个要素
明确的初始状态
"Given 用户已登录"太弱——以什么角色登录?具备什么权限?测试数据库里有没有数据,还是空的?
强的前置条件会把相关状态一条条列出来:
Given a user account with role "admin" in workspace "acme", AND the workspace has 3 projects, AND project 2 has status "archived"
是的,字数更多了。但这些本来就是 QA 会在 Slack 上问你的问题。在规格里写一次就好。
输入,而非意图
"用户搜索最近的订单"描述的是意图。"用户向 /orders?date_range=last_7_days&status=open&limit=20 发送一个 GET 请求"描述的是输入。
如果 API 形状还没定下来,规格不必规定具体的 URL。但它必须对用户能问什么做出承诺。"最近的订单"不是输入——它是一个概念,而实现方会自行挑一个解释。
带阈值的可观察结果
大多数规格就是在这一步变得不可测试的。"可接受的延迟"、"合理的默认值"、"优雅降级"——这些都不是结果,它们是感觉。
把每一个形容词都换成数字或枚举:
- "快" → "10KB 负载下 p95 小于 300ms"
- "可靠" → "任意 30 天滚动窗口内成功响应率 99.9%"
- "合理的重试" → "在向用户暴露错误之前,以指数退避(1s、2s、4s)重试 3 次"
- "恰当的错误处理" → "每种失败模式对应一个具体的响应状态码和消息,在下方错误表中逐一列出"
检查本身
如果结果是可观察的,QA 就需要知道怎么去观察它。对 UI 功能,指名元素或文案。对 API 功能,指名状态码和响应结构。对异步功能,指名最终状态和最大等待时间。
"展示错误消息"还不够。"提交后 2 秒内,表单上方出现一条错误 banner,文案为'Card declined — please use a different payment method'"才够。
非功能性需求,以可测试的方式
那些常见的非功能性需求——性能、安全、可靠性、可访问性——正是规格最爱含糊的地方。它们本不必如此。
- 性能。写明百分位和负载。"10KB 负载下 p95 延迟 < 300ms,在负载均衡器处测量,5 分钟窗口。"可测试。
- 安全。列出应该通过的检查。"SQL 注入测试套件通过、XSS 测试套件通过、所有状态变更路由必须带 CSRF token、/login 每个 IP 限速 100 req/min。"可测试。
- 可靠性。写明 SLO 和度量方式。"30 天滚动窗口内,99.9% 的 /checkout 请求返回 2xx 或 4xx(而非 5xx)。"可测试。
- 可访问性。指明等级。"结账流程通过 axe-core 的 WCAG 2.1 AA 级自动化审计。"可测试。
重写测试法
拿起规格里的任何一条声明,试着把它转成一个测试函数签名。转不出来,就还不可测试。
// Untestable claim:
// "The system handles errors gracefully"
// Test signature: ??? (no input, no expected output)
// Testable claim:
// "On backend 503, the UI shows the retry banner within 2s and enables
// a 'Try again' button that resubmits the last request"
test('shows retry banner on 503 within 2s', async () => {
mockBackendResponse(503);
await submitForm();
expect(screen.queryByText('Service unavailable')).toBeVisible({ timeout: 2000 });
expect(screen.queryByRole('button', { name: 'Try again' })).toBeEnabled();
});
我审稿时看什么
审阅规格的可测试性时,我会在心里把每个形容词和每个被动结构都高亮出来。这两者通常都是规格在做一项自己无法核验的声明的地方。有时形容词没问题,因为前面已经给出了可衡量的定义。更多时候并没有。
快速清单:
- 我能数出可测试声明的条数吗?目标是"每条验收标准至少一条"。
- 数字带单位了吗?"200"不是延迟阈值,"200ms"才是。
- QA 不用问"我们"指谁就能跑这个检查吗?Persona 可以,"我们"不行。
- 错误路径是不是配了状态码和消息逐一写清,还是一句"处理错误"带过?
真正能感受到的收益
可测试的规格不只帮 QA。它缩短实现周期,因为工程师不再打断产品去问"合理"到底是什么意思。它缩短评审周期,因为评审者有了具体的东西可以反驳。它还减少上线后的意外,因为团队在发布前就已经为那些数字吵过一架,而不是发布后。
代价是多花一个小时写。省下的是三周后那场四小时的会议——大家坐在那里,试图搞清楚这个功能原本到底是要做什么。
评审时看什么
这篇文章适合用在把规格改到 QA 可以直接写测试时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。
- 每条 AC 旁边标证据类型。
- 把内部实现描述改成外部可见行为。
- 写清测试起始数据、权限和状态。
- 把“处理错误”改成具体错误码、提示或回滚结果。
可测试规格的目标很朴素:QA 不需要追着作者问“这句话到底怎么看算通过”。
例子:不要写“导出要足够快”,改成“拥有 5 万行数据的用户在报表页导出 CSV,20 秒内拿到可下载文件;生成失败时看到可重试错误”。这句话同时给工程目标、QA 用例和客服解释口径。
落地例子
以密码重置为例,弱规格会写“邮件要及时发送,链接要安全”。可测试规格会写:token 30 分钟过期,只能使用一次;账号不存在时返回中性提示;每次请求写入安全事件但不暴露邮箱是否存在;连续请求触发限流。QA、安全和工程都能根据这些句子直接工作。
这类细节不是额外文档,而是产品行为本身。规格如果不写,代码也会有答案,只是答案由实现者临场决定。
再做一次反向检查:把规格里的每个形容词圈出来,比如“快速”“稳定”“安全”“友好”。如果这个词不能变成一个测试步骤、一个阈值或一个失败提示,就把它改写。测试性不是文风问题,而是交付团队能否在同一条线上验收。
最后把这些检查贴到 PR 描述里。评审者看到证据链接,就能判断规格是否真的被实现。
可测试规格一定有可观察输出
“系统正确处理”不可测试。可测试的写法会说清楚 API 响应、数据库状态、事件、日志或 UI 文案。至少要有一个外部可观察证据,否则 QA 只能问开发。
Untestable: - System handles expired invite links. Testable: - Given an invite expired 24 hours ago When the user opens the link Then the API returns 410 with code invite_expired And no membership row is created And the UI shows "Ask the admin for a new invite"
边界:不是所有内部实现都要暴露给测试。测试看行为和证据,不看私有函数名。
测试矩阵可以很小
一份小 spec 只要三列:场景、预期证据、测试 owner。比如 API 返回、数据库行、事件、UI 状态各写一条。矩阵不是替代测试代码,而是让大家在实现前同意“什么证据算通过”。
对每条标准都问一句:失败时谁能看见?用户、API 调用方、日志、数据库表、监控还是客服后台。看不见的行为很难测试,也很难运营。规格要把可见性写出来。
规格里还可以加一列“测试位置”:unit、API contract、integration、E2E、manual QA。不同证据放在不同层级,owner 才清楚谁负责补测试代码、谁负责准备数据、谁负责验收 UI 状态。
如果某条标准找不到测试位置,就说明它还不是可测试规格。把它改成字段、状态、API 响应、数据库记录、事件或日志证据,再决定由哪个 owner 负责验证。这样规格会短一点,但更硬。
最后再补一个失败样本。没有失败样本的规格,常常只证明 happy path。失败样本能逼出 error code、空状态、权限状态和恢复路径。
如果还是测不了,就把这条从验收标准里拿出来,改成开放问题交给 owner 决定。可测试性不是文案润色,而是实现前的风险控制。
先问清楚,再写代码和用例,不要靠测试阶段补洞,也不要拖到上线前。
可复制产物:规格写作片段
当工单看似清楚但还缺验收语言时使用。它要求作者写出角色、触发、结果和证据。
规格写作评审片段:如何撰写可测试的软件规格文档 本次要做的决策: - 把模糊需求改写成可以由工程和 QA 共同判断的验收条款。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 写作边界:避免模糊动词,每条验收标准都要有可见的通过或失败信号。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
二次审阅记录:可测试不等于更长
这次复看是为了避免文章听起来像是在要求更多文档。可测试规格通常更短,只是把状态和证据写得更准。
改写规则: - 用可观测结果替换形容词。 - 用状态变化替换“能工作”。 - 只有阈值会改变设计时,才写“多快”。 - 用具体 UI、API 或日志行为替换“优雅处理”。
编辑复核记录
复核日期:2026-04-28。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 验收标准 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
最近复核:2026-04-28。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文面向软件交付团队,介绍如何撰写可测试的软件规格文档。示例均为工程场景说明,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:文章审阅与更新方式
- 纠错:联系编辑