验收标准示例——20 个可直接复制的真实模板
网上大多数验收标准示例都太浅,没法直接用到实际项目中。本文给你 20 个生产级的真实示例,覆盖认证、电商、API、数据处理和通知五大领域——全部采用 Given/When/Then 格式,可以直接复制到你的规格文档里。
现场笔记:最能抓 bug 的不是正常路径
我最常用的一类验收标准,不是“成功提交”。而是重复动作:按钮连点、webhook 重复投递、支付重试、导入行被看见两次。这个场景比一堆正常路径更容易暴露规格里的含糊。
重复动作验收标准: Given 用户在 2 秒内提交两次相同请求 When 两个请求都到达服务端 Then 只记录一次状态变更 And 第二次响应返回第一次操作的 id And 审计日志标记为 replay
什么是验收标准?
验收标准是一组具体的、可测试的条件,功能必须满足这些条件才能算"完成"。它不是用户故事——用户故事描述的是谁想要什么、为什么;验收标准描述的是你如何验证故事被正确实现了。它也不是测试用例——测试用例是 QA 逐步执行的操作步骤,而验收标准是测试用例的来源依据。
可以把验收标准理解为连接"我们想要什么"和"我们怎么证明它行了"之间的桥梁。一个用户故事可能写着:"作为客户,我想要重置密码,以便重新访问我的账户。"验收标准则具体定义了"重置密码"在可观测、可验证的层面意味着什么:什么触发重置、系统发什么、链接有效多久、链接过期后怎么处理。
常见格式有两种。第一种是 Given/When/Then(也叫 Gherkin 格式),结构化程度高,适合有明确前置条件、操作和预期结果的行为描述。第二种是清单格式——简单的勾选列表,每条是一个是/否判断。清单格式适合简单的配置类需求("按钮是蓝色的"、"该字段最多接受 255 个字符"),但凡涉及条件逻辑或状态转换,Given/When/Then 会更精确。
本文聚焦 Given/When/Then 格式,因为它是能规模化的写法。当你的功能有多条用户路径、多种错误情况和各种边界条件时,结构化的格式会迫使你把每一种情况都写清楚。这种精确性正是区分"工程师能直接动手"和"先在 Slack 上问三天"的关键差异。
Given/When/Then 格式详解
每条 Given/When/Then 标准由三部分组成,各有分工:
- Given——起始状态或前置条件。它建立了行为发生之前的上下文。"Given 用户已登录。""Given 购物车中有两件商品。""Given API 速率限制设置为每分钟 100 次请求。"Given 子句回答的问题是:在操作发生之前,什么必须为真?
- When——操作或触发动作。这是用户或系统执行的具体行为。"When 用户点击提交。""When 定时任务在 UTC 午夜运行。""When API 收到一个 body 为空的 POST 请求。"When 子句回答的问题是:什么事件引发了行为?
- Then——可观测的、可验证的结果。这是测试者能看到、测量或查询到的东西。"Then 订单确认页面显示订单号。""Then API 返回 HTTP 422 及 JSON 错误体。""Then 邮件在 60 秒内发送。"Then 子句回答的问题是:我们怎么知道它成功了?
下面是一个带注释的示例:
Given 一个已注册用户正在登录页面
^ 前置条件:用户已存在,页面已加载
When 用户输入有效邮箱和正确密码,并点击"登录"
^ 操作:具体的输入 + 具体的触发
Then 用户在 2 秒内被重定向到仪表盘;
会话 cookie 被设置为 HttpOnly 和 Secure 标志;
users 表中的 last_login_at 时间戳被更新为当前 UTC 时间。
^ 结果:三条可观测、可测试的输出
好的验收标准最重要的特质是:QA 能直接根据它写出测试,不用去问作者"你到底是什么意思"。如果标准写的是"系统响应很快"——这没法测,因为"快"可能是 200 毫秒也可能是 5 秒,取决于谁在读。如果写"API 在 P95 下响应时间不超过 500ms",QA 就知道该测什么。
下面是一个并排对比,让差异更直观:
差的写法
"登录功能应该正常工作,并优雅地处理错误。"
这告诉 QA 什么?"正常"是什么意思?"优雅"长什么样?读到这条的工程师只能自己发明行为。
好的写法
"Given 用户连续输错密码三次,When 用户第四次尝试登录,Then 账户被锁定 15 分钟;登录表单显示'账户已锁定,请 15 分钟后重试';在锁定期满之前不再处理任何身份验证请求。"
QA 只看这一句话就能写出自动化测试。
格式讲清楚了,接下来看五个领域、20 个真实示例。每一个的细节程度都足够工程师直接实现、QA 直接测试,无需再额外沟通。
认证示例(1–4)
认证是大多数应用的起点,也是模糊验收标准导致安全漏洞最多的地方。"用户可以登录"不是验收标准——那是一个愿望。下面四个示例覆盖了最常见的认证流程,具备实现和测试所需的精确度。
示例 1:使用有效凭证登录
Given 一个已注册用户,邮箱为 "[email protected]",账户已验证
When 用户导航到 /login,
在邮箱字段输入 "[email protected]",
输入正确的密码,
并点击"登录"
Then 用户在 2 秒内被重定向到 /dashboard;
一个名为 "sid" 的会话 cookie 被设置,属性如下:
- HttpOnly: true
- Secure: true
- SameSite: Strict
- Max-Age: 86400(24 小时);
数据库中该用户的 last_login_at 字段
被更新为当前 UTC 时间戳;
一条登录事件被写入 audit_log 表,
action="login_success",ip_address 已填充。
示例 2:密码错误——账户锁定
Given 一个已注册用户,邮箱为 "[email protected]",
且账户当前未被锁定
When 用户在 10 分钟窗口内连续提交 3 次错误密码
Then 账户被锁定 15 分钟;
第四次及之后的登录尝试返回 HTTP 429,
body 为 {"error": "account_locked", "retry_after_seconds": 900};
登录表单显示"账户已锁定,请 15 分钟后重试。";
一条 login_lockout 事件被写入 audit_log 表;
15 分钟后账户自动解锁,
用户可以使用正确密码重新登录。
示例 3:密码重置邮件
Given 一个已注册用户,邮箱为 "[email protected]"
When 用户导航到 /forgot-password,
输入 "[email protected]",
并点击"发送重置链接"
Then 系统在 60 秒内向 "[email protected]" 发送一封邮件,
主题为"重置你的密码";
邮件中包含一个一次性重置链接,格式为
https://app.example.com/reset-password?token={token};
token 自签发之时起有效期为 1 小时;
/forgot-password 页面显示
"如果该邮箱对应的账户存在,我们已发送重置链接。"
无论该邮箱是否存在(防止枚举攻击);
如果用户在 1 小时内点击重置链接,
将进入一个输入新密码的表单;
如果用户在 1 小时后点击重置链接,
将看到"此链接已过期。请重新申请。"
并附带一个指向 /forgot-password 的链接;
重置链接成功使用后,
同一 token 不能再次使用。
示例 4:会话因不活动而过期
Given 用户已登录且会话处于活跃状态,
会话超时配置为 30 分钟无操作
When 用户连续 30 分钟未执行任何操作
(无 API 调用、无页面导航)
Then 用户的下一次请求返回 HTTP 401;
服务端的会话 cookie 被清除;
用户被重定向到 /login,带查询参数
?reason=session_expired;
登录页面显示"你的会话因不活动已过期。";
客户端上任何未保存的表单数据不可恢复
(这是已知限制,已在 UI 中标注)。
电商示例(5–8)
电商功能涉及金钱、库存和客户预期——这三样东西中,任何一点模糊都会直接变成收入损失或客服工单。下面的示例覆盖了购物车操作、折扣码、库存边界和交易邮件。
示例 5:加入购物车
Given 一个客户正在查看商品 SKU-1042("无线键盘"),
该商品有库存,available_quantity = 35,
商品价格为 $49.99
When 客户点击"加入购物车",数量 = 1
Then 页头的购物车商品数增加 1;
购物车包含一条 SKU-1042 的行项,信息如下:
- quantity: 1
- unit_price: $49.99
- line_total: $49.99;
购物车小计被重新计算并显示;
一个简短的确认提示显示 3 秒:
"无线键盘已加入购物车";
此时商品的 available_quantity 不扣减
(库存仅在结账时预留);
如果客户对同一 SKU 再次点击"加入购物车",
已有行项的数量增加为 2,
而不是创建一条重复的行项。
示例 6:使用折扣码
Given 一个客户的购物车小计为 $120.00,
存在一个有效折扣码 "SAVE20",属性如下:
- type: percentage
- value: 20%
- minimum_order: $50.00
- max_uses: 500(当前使用次数:312)
- expires_at: 2026-12-31T23:59:59Z
When 客户在折扣码输入框中输入 "SAVE20" 并点击"应用"
Then 折扣金额 $24.00($120.00 的 20%)被应用;
购物车显示:
- 小计:$120.00
- 折扣 (SAVE20):-$24.00
- 合计:$96.00;
折扣码字段显示绿色勾号和文字
"SAVE20 已应用——你节省了 $24.00";
折扣码的使用次数仅在下单时才从 312 增至 313
(不是在应用到购物车时);
如果客户移除商品导致小计低于 $50.00,
折扣自动移除,客户看到
"折扣码 SAVE20 要求最低订单金额为 $50.00。"
示例 7:库存不足时结账
Given 客户购物车中有 3 件 SKU-2087,
而 SKU-2087 当前的 available_quantity 为 1
(库存在商品加入购物车后减少了)
When 客户在结账页面点击"下单"
Then 订单不会被创建;
结账页面显示错误横幅:
"购物车中部分商品的库存已不足。";
SKU-2087 的购物车行项显示警告:
"仅剩 1 件——请更新数量。";
该行项的数量字段被高亮为红色;
客户可以将数量改为 1 并重新结账;
如果在页面加载和提交之间 available_quantity 降为 0,
错误信息变为"SKU-2087 已售罄",
该行项只显示一个"移除"按钮;
在任何库存不足的场景中都不会扣款。
示例 8:订单确认邮件
Given 邮箱为 "[email protected]" 的客户下了订单 #ORD-88421,
包含 2 个行项:
- SKU-1042 "无线键盘" x1,$49.99
- SKU-3001 "USB-C 数据线" x2,每条 $12.99
已使用折扣码 "SAVE20"(-$15.19),
配送方式为"标准快递(5-7 个工作日)",运费 $5.99
When 付款成功扣款后
Then 一封订单确认邮件在付款后 120 秒内
发送到 "[email protected]";
邮件主题为"订单 #ORD-88421 已确认";
邮件正文包含:
- 每个行项的名称、数量、单价和行合计
- 小计:$75.97
- 折扣:-$15.19
- 运费:$5.99
- 订单总计:$66.77
- 预计到货日期范围
- 指向订单状态页面的链接:/orders/ORD-88421;
邮件发件人为 "[email protected]",
回复地址为 "[email protected]";
如果邮件发送失败(SMTP 错误),
使用指数退避重试 3 次(1 分钟、5 分钟、15 分钟);
如果全部重试均失败,向运维频道发送告警,
并将失败记录写入 email_delivery_log 表。
API 示例(9–12)
API 验收标准是工程师最常写不清的地方。"端点返回数据"不是标准。API 有状态码、响应头、分页契约、错误格式和速率限制——这些都需要在规格中定义,否则调用方只能在生产环境中靠试错来了解行为。
示例 9:GET 端点返回分页结果
Given 数据库中有 250 条产品记录,
且 API 调用方发送了有效的 Bearer token
When 调用方发送 GET /api/v2/products?page=2&per_page=25
Then 响应状态码为 200 OK;
响应体为 JSON,结构如下:
{
"data": [ ...25 个产品对象... ],
"meta": {
"current_page": 2,
"per_page": 25,
"total_items": 250,
"total_pages": 10
}
};
每个产品对象至少包含:
id, name, sku, price, currency, created_at;
默认按 created_at 降序排列(最新优先),
除非请求中提供了 ?sort= 参数;
响应包含 header:
Link: </api/v2/products?page=1&per_page=25>; rel="prev",
</api/v2/products?page=3&per_page=25>; rel="next";
如果 page 超过 total_pages(例如 ?page=99),
响应仍为 200,"data" 为空数组,
meta.total_pages 仍然是 10;
per_page 上限为 100;
超过 100 的值静默截断为 100,不报错。
示例 10:POST 请求体不合法返回 422
Given API 调用方发送了有效的 Bearer token
When 调用方发送 POST /api/v2/products,JSON body 为:
{
"name": "",
"sku": "ALREADY-EXISTS-001",
"price": -5.00
}
Then 响应状态码为 422 Unprocessable Entity;
响应体为 JSON,结构如下:
{
"error": "validation_failed",
"message": "请求体包含无效字段。",
"details": [
{"field": "name", "issue": "不能为空"},
{"field": "sku", "issue": "已存在"},
{"field": "price", "issue": "必须大于或等于 0"}
]
};
所有校验错误在一次响应中全部返回
(而不是一次只返回一个);
Content-Type 头为 "application/json; charset=utf-8";
数据库中没有创建任何产品记录;
错误响应被记录日志,附带 request_id 以便追踪。
示例 11:速率限制——N 次请求后返回 429
Given API "standard" 套餐的速率限制为每分钟 100 次请求,
且调用方在当前 60 秒窗口内已使用 API key "key_abc123"
发出了 100 次请求
When 调用方在同一窗口内发出第 101 次请求
Then 响应状态码为 429 Too Many Requests;
响应体为:
{
"error": "rate_limit_exceeded",
"message": "已超过每分钟 100 次请求的速率限制。",
"retry_after_seconds": <当前窗口剩余秒数>
};
响应包含以下 header:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: <窗口重置的 Unix 时间戳>
Retry-After: <距离重置的秒数>;
所有成功响应(非 429)同样包含
X-RateLimit-Limit 和 X-RateLimit-Remaining header,
方便调用方主动跟踪用量;
速率限制按 API key 隔离,不按 IP 地址;
返回 429 的请求不计入下一个窗口的配额。
示例 12:API 版本管理——废弃端点警告
Given API v1 端点 GET /api/v1/products 已被标记为废弃,
替代端点为 GET /api/v2/products,
v1 下线日期为 2026-09-01
When 调用方发送 GET /api/v1/products
Then 响应状态码为 200 OK(端点仍然可用);
响应体结构与当前 v1 契约完全一致
(下线前不做破坏性变更);
响应包含以下 header:
Deprecation: true
Sunset: Sat, 01 Sep 2026 00:00:00 GMT
Link: </api/v2/products>; rel="successor-version";
API 记录一条 deprecation_warning 事件,包含:
api_key, endpoint, timestamp;
在下线日期(2026-09-01T00:00:00Z)之后,
对 /api/v1/products 的请求返回 410 Gone,body 为:
{
"error": "endpoint_removed",
"message": "此端点已于 2026-09-01 下线。
请使用 /api/v2/products。",
"migration_guide": "https://docs.example.com/api/v1-to-v2"
}。
数据处理示例(13–16)
数据处理功能——导入、批量作业、数据迁移——是验收标准缺失造成损害最大的地方。当一个批量任务在 50,000 行文件的第 40,000 行失败,而没人事先定义过失败时该怎么办,团队只能在凌晨两点的 on-call 事故中亲自发现这个缺口。以下示例定义了成功、失败和部分失败时的行为。
示例 13:CSV 导入与重复检测
Given 管理员在 /admin/contacts/import 上传了一个 CSV 文件,
包含 10,000 行,每行包含:first_name, last_name, email,
其中 200 行的邮箱地址已存在于 contacts 表中
When 导入任务处理该文件
Then 创建 9,800 条新联系人记录;
200 行被标记为重复并跳过(不更新);
导入结果页面显示:
- 总行数:10,000
- 已创建:9,800
- 跳过重复:200
- 错误:0;
可在 /admin/contacts/import/{job_id}/skipped 下载
200 行被跳过记录的 CSV,
包含列:row_number, email, reason;
邮箱比对不区分大小写
("[email protected]" 匹配 "[email protected]");
导入任务的处理速率至少为 500 行/秒;
如果 CSV 中存在格式错误的行(缺少必填 email 列),
该行计入"错误",其余行继续处理;
导入按行原子提交,不按文件原子提交:
如果任务在第 5,000 行中断,
前 5,000 行保持已提交状态。
示例 14:批量任务超时处理
Given 一个名为 "generate-monthly-invoices" 的夜间批量任务
定时在 UTC 02:00 运行,
超时时间配置为 45 分钟
When 任务运行了 45 分钟仍未完成
Then 任务以 "timed_out" 状态被终止;
超时前已生成的所有发票被提交
(部分进度被保留);
一条 job_timeout 事件被写入 batch_jobs 表,包含:
job_name, started_at, timed_out_at,
rows_processed, rows_remaining;
向 #ops-alerts 频道发送告警,信息为:
"批量任务 'generate-monthly-invoices' 在 45 分钟后超时。
已处理:{n}/{total}。需要人工介入。";
任务不会自动重试
(防止生成重复发票);
管理员可以通过 POST /admin/batch-jobs/{job_id}/resume
触发可恢复的重新运行,
仅处理剩余未处理的记录。
示例 15:数据迁移回滚
Given 数据迁移 "migrate_users_v2" 正在转换 users 表,
将 "name" 列拆分为 "first_name" 和 "last_name",
已对 80,000 行中的 50,000 行完成迁移
When 迁移遇到一行 name 字段中没有空格的记录
(例如 "Madonna"),拆分逻辑失败
Then 迁移停止,不再处理剩余行;
已迁移的 50,000 行保持新格式
(不会自动回滚);
迁移状态设为 "failed_at_row",元数据包含:
row_id, error_message, rows_completed, rows_remaining;
运维团队收到包含失败详情的告警;
管理员可以执行回滚命令:
POST /admin/migrations/migrate_users_v2/rollback,
将已完成的 50,000 行通过拼接
first_name + " " + last_name 恢复到 name 列;
回滚操作是幂等的:执行两次结果相同;
回滚后,一个验证查询确认所有行
与迁移前状态一致,对比依据是
migration_snapshots 表(迁移开始前创建)。
示例 16:联系人去重
Given 同一个账户下存在两条联系人记录:
联系人 A:id=1001, email="[email protected]",
name="Alice Smith", phone=null,
created_at="2026-01-15"
联系人 B:id=1002, email="[email protected]",
name="A. Smith", phone="+1-555-0100",
created_at="2026-03-20"
When 该账户的去重任务运行
Then 两条联系人被合并为一条记录;
存活记录为联系人 A(created_at 更早);
合并逻辑用重复记录的值填充存活记录的空字段:
联系人 A 的 phone 被设为 "+1-555-0100"(原为 null);
两条记录中都有值的字段不会被覆盖:
联系人 A 的 name 保持 "Alice Smith"
(不会被 "A. Smith" 替换);
联系人 B 被软删除(del_flag = 1),并引用
存活记录:merged_into_id = 1001;
一条 dedup_event 被记录,包含:
source_id=1002, target_id=1001,
merged_fields=["phone"],
trigger="scheduled_job", account_id, created_at;
任何引用联系人 B ID 的外部记录(如 deal 记录)
被更新为指向联系人 A 的 ID;
如果联系人 B 有活跃的自动化流程引用其 ID,
合并被阻止,改为创建一条 dedup_conflict 记录。
通知示例(17–20)
通知类需求始终被写得不够充分,因为团队觉得它很简单——"发个通知"。但通知涉及发送渠道、用户偏好、时序、失败处理和状态管理。如果没有定义重试行为,用户要么收不到通知,要么连收五条。
示例 17:推送通知投递
Given 用户已在移动设备上开启推送通知,
用户的设备 token 已注册在 push_tokens 表中,
用户没有将"订单"类通知设为静音
When 用户的订单状态从 "shipped" 变为 "delivered"
Then 在状态变更后 30 秒内,向该用户的所有已注册设备
发送推送通知;
通知 payload 包含:
title: "订单已签收"
body: "你的订单 #ORD-88421 已签收。"
data: {"order_id": "ORD-88421",
"deep_link": "/orders/ORD-88421"};
点击通知后打开 App 并跳转到订单详情页;
如果设备 token 无效(提供商返回 "unregistered"),
从 push_tokens 表中删除该 token,
且不对该 token 进行重试;
如果推送提供商返回瞬时错误(5xx),
以 30 秒间隔重试最多 3 次;
每次投递尝试被记录到 notification_log 表,
包含:user_id, channel="push", status,
attempt_number, timestamp。
示例 18:邮件通知偏好——退订
Given 用户通过 /settings/notifications 通知偏好页面
退订了"营销"类邮件,
但没有退订"事务性"邮件
When 系统触发一封营销邮件(如"本月新功能")
Then 该邮件不发送;
退订行为被记录到 email_log 表,包含:
user_id, email_type="marketing",
status="suppressed_by_preference";
不产生退信或错误(这是预期行为)。
Given 同一用户触发了一封事务性邮件
(如"你的密码已更改")
When 邮件服务处理该触发
Then 邮件被发送,不受营销退订的影响,
因为事务性邮件不受偏好设置控制;
邮件页脚包含指向 /settings/notifications 的链接,
但不包含事务性邮件的"取消订阅"链接
(按 CAN-SPAM 法规,事务性邮件免于退订要求);
投递被记录,email_type="transactional",
status="sent"。
示例 19:应用内通知角标计数
Given 用户在 notifications 表中有 5 条未读通知
(read_at IS NULL 且 user_id 匹配)
When 用户加载应用的任意页面
Then 页头的通知铃铛图标显示一个角标,数字为 "5";
角标为红色(#E53E3E),白色文字;
如果计数超过 99,角标显示 "99+";
用户打开通知下拉菜单时,
显示 5 条最新的未读通知,
按 created_at 降序排列;
用户点击一条通知时,
该通知的 read_at 被设为当前时间戳,
角标数字减 1;
用户点击"全部已读"时,
该用户的所有通知 read_at 被设为当前时间戳,
角标消失;
角标计数通过 WebSocket 实时更新
(如果页面打开时收到新通知,
计数自动增加,无需刷新页面);
如果用户有 0 条未读通知,不显示角标
(铃铛图标不带数字)。
示例 20:通知发送失败后的重试
Given 通知服务尝试通过 SMTP 提供商向 "[email protected]"
发送一封邮件通知
When SMTP 提供商返回瞬时错误
(连接超时、5xx 响应或 DNS 解析失败)
Then 该通知被放入重试队列;
重试采用指数退避策略:
- 第 1 次重试:1 分钟后
- 第 2 次重试:5 分钟后
- 第 3 次重试:15 分钟后
- 第 4 次重试:60 分钟后;
每次重试被记录到 notification_log 表,包含:
notification_id, attempt_number, status="retry",
error_message, next_retry_at;
第 4 次重试仍失败后,通知状态设为
"permanently_failed";
permanently_failed 事件触发向 #ops-alerts 频道
发送告警,包含:notification_id, user_id,
channel, error_history;
如果 SMTP 提供商返回永久性错误
(550 "mailbox not found"、551 "user not local"),
不进行重试;
通知状态立即设为 "permanently_failed";
该用户的 email_verified 标志设为 false,
下次登录时显示"请验证你的邮箱"横幅。
验收标准的 5 个常见错误
即使是一直在用 Given/When/Then 格式的团队,也会犯同样的一组错误。以下五种反模式几乎出现在我做过的每次 spec 评审中。每个都配了具体的修正写法。
1. 太模糊——"系统正常工作"
差的写法
"搜索功能应该正常工作,返回相关的结果。"
好的写法
"Given 产品目录包含 5,000 个商品,When 用户搜索'无线键盘',Then 结果页面在 800ms 内显示,展示名称或描述中包含搜索词的产品,按相关性评分降序排列,每页最多 20 条结果。"
"正常工作"不是可测试的陈述。用具体的输入、可度量的输出和明确的阈值来替换它。如果你无法用可观测的术语描述"正确"长什么样,说明你还没想清楚这个需求。
2. 写了实现细节而不是行为——"用 Redis 缓存"
差的写法
"系统将产品数据缓存在 Redis 中,TTL 为 5 分钟,产品更新时使缓存失效。"
好的写法
"Given 某产品价格从 $49.99 更新为 $39.99,When 客户在更新后 5 分钟内查看该产品,Then 显示的价格为 $39.99。非价格字段(描述、图片)可以有最长 5 分钟的旧数据。"
验收标准描述的是用户或 API 调用方观察到的行为,而不是系统内部如何构建。Redis 是一个实现选择——标准应该描述用户体验到的行为。工程团队自行决定用 Redis、Memcached 还是进程内缓存来满足标准。
3. 多个行为塞进一条标准
差的写法
"Given 用户提交表单,Then 数据被校验、保存到数据库、发邮件给管理员、用户看到成功页面、并且触发分析事件。"
好的写法
拆分为多条标准:
"Given 用户填写了所有必填字段并提交联系表单,When 表单被提交,Then 用户在 1 秒内被重定向到 /thank-you。"
"Given 一条联系表单提交被保存,When 保存完成后,Then 一封包含提交详情的邮件在 60 秒内发送到 [email protected]。"
"Given 用户提交联系表单,When /thank-you 页面加载,Then 触发一个 'form_submitted' 分析事件,包含 form_id 和 timestamp。"
每条标准应该测试一个行为。当你把五个行为合并成一条标准时,测试失败也说不清是哪个行为出了问题。分开写可以实现针对性测试和更清晰的 bug 报告。
4. 只写了正常路径——没有错误和边界情况
差的写法
"Given 用户上传头像,Then 头像被保存并显示在个人资料上。"
好的写法
正常路径标准加上:
"Given 用户上传的文件不是图片(如 .pdf),Then 上传被拒绝,提示'请上传 JPG、PNG 或 WebP 格式的图片。'"
"Given 用户上传的图片大于 5 MB,Then 上传被拒绝,提示'图片大小不能超过 5 MB。'"
"Given 用户上传了有效图片但存储服务不可用,Then 用户看到'上传失败,请重试。'并且失败被记录日志。"
只写正常路径意味着你的测试只能通过那个没人担心的场景。Bug 藏在错误路径、边界条件和故障模式里。把那些场景的标准也写出来。
5. 不可测试的性能描述——"性能很快"
差的写法
"仪表盘在大数据集下也能快速加载。"
好的写法
"Given 账户下有 100,000 个联系人,When 用户导航到 /dashboard,Then 页面在 4G 网络下 3 秒内可交互(Time to Interactive)。联系人汇总组件在 2 秒内加载。活动动态异步加载,数据到达前显示骨架屏。"
"快"和"迅速"不是度量指标。定义数据集大小、网络条件和具体指标(Time to Interactive、服务器响应时间、总加载时间)。没有数字,性能标准只是没人能测试的美好愿望。
验收标准空白模板
在为自己的功能编写验收标准时,可以使用下面的模板。复制这个区块,用你的具体前置条件、操作和预期结果替换括号中的提示,然后根据功能需要添加足够的标准。一个典型的功能需要 4 到 8 条标准覆盖主要流程,再加 2 到 4 条覆盖错误和边界情况。
## 验收标准
### 正常路径
Given [前置条件——系统或用户的起始状态]
When [操作——用户或系统执行的具体行为]
Then [结果——可观测的、可验证的输出];
[需要时的额外结果];
[需要时的额外结果]。
Given [不同的前置条件]
When [操作]
Then [结果]。
### 错误处理
Given [导致错误状态的前置条件]
When [与正常路径相同或类似的操作]
Then [用户看到或 API 返回的具体错误];
[什么不受影响——例如"数据不被修改"];
[错误如何被记录或上报]。
### 边界情况
Given [边界条件——空输入、最大值、并发访问]
When [操作]
Then [此边界情况的具体行为]。
### 性能
Given [真实的数据集大小或负载条件]
When [操作]
Then [可度量的性能阈值——例如响应时间、吞吐量]。
如果你想用工具从一段简短的功能描述自动生成完整的 spec——包括验收标准、边界条件和交付物——规格生成器可以在几秒内完成。
自动生成验收标准
手写 20 条验收标准对学习格式很有用。但日常工作中,你需要一个更快的起点。本站的规格生成器只需要一段简短的功能描述——一两句话说明功能要做什么——就能生成一份结构化的 spec,验收标准已经用 Given/When/Then 格式写好了。
生成的标准是初稿,不是终稿。它给你一个可以在 spec 评审中编辑、扩展和打磨的基础结构。大多数团队发现,从生成的初稿开始能将 spec 编写时间缩短一半,因为最难的部分不是填细节——而是盯着空白页不知道从哪开始。
这个工具免费,在浏览器中运行,不需要注册账号。
前后对比:把一句模糊标准改成测试
我最常做的修复,是把“用户可以做某事”改成状态、操作和可观察结果。下面是一个成员邀请流程的差别。
修改前: - 管理员可以邀请团队成员。 修改后: - Given 工作区管理员输入一个新成员邮箱 When 提交邀请 Then 系统只创建一条 status="pending" 的邀请 And 邀请邮件包含一次性 token And 再次提交同一邮箱时返回已有 pending 邀请
第二种写法一次性给出了重复提交、安全 token 和状态变化,QA 不需要再追问作者才能写测试用例。
可复制产物:规格写作片段
当工单看似清楚但还缺验收语言时使用。它要求作者写出角色、触发、结果和证据。
规格写作评审片段:验收标准示例——20 个可直接复制的真实模板 本次要做的决策: - 把模糊需求改写成可以由工程和 QA 共同判断的验收条款。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 写作边界:避免模糊动词,每条验收标准都要有可见的通过或失败信号。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
旗舰使用路径
这是 Spec Coding 用来承接「验收标准示例库」主题的核心参考页之一。建议把它放到真实工单、PR 或发布评审里使用,而不是只当背景文章阅读。
- 适合从这里开始:工单有意图,但 QA 需要反复追问作者才能执行。
- 建议复制:一条 Given/When/Then 模式,以及对应的失败路径版本。
- 需要附上的证据:每条标准都对应测试用例、人工 QA 记录或截图证据。
- 搭配使用:验收标准 Hub 与 Gherkin 生成器。
旗舰页使用路径: - 在计划或评审时打开本文。 - 把对应产物复制到工单或 PR。 - 用自己的系统、责任人和失败模式替换示例值。 - 如果证据行仍为空,就不要进入实现。
二次审阅记录:示例必须教会一种模式
这次检查的重点是:示例不能只是可复制文本,还要让读者学会一种测试模式,比如身份、状态、非法输入、时序、权限或重试。
示例复核: - 保留能写明操作者、触发动作和可观测结果的示例。 - 一个锋利失败路径,胜过三个泛泛正常路径。 - 高风险场景补 replay、timeout、permission 或 partial success。 - 删除只是复述功能标题的标准。
编辑复核记录
复核日期:2026-04-29。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入验收标准主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
延伸阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
本文讲解的是面向软件交付团队的验收标准编写方法。所有示例均为工程场景演示,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:我们如何审核与更新文章
- 纠错反馈:联系编辑