验收标准示例——20 个可直接复制的真实模板

验收标准示例——20 个可直接复制的真实模板
Daniel Marsh · Spec-First 工程笔记

网上大多数验收标准示例都太浅,没法直接用到实际项目中。本文给你 20 个生产级的真实示例,覆盖认证、电商、API、数据处理和通知五大领域——全部采用 Given/When/Then 格式,可以直接复制到你的规格文档里。

发布于 2026-04-08 · ✓ 已更新 2026-05-06 · 阅读约 20 分钟 · 作者:Daniel Marsh · 审稿政策:编辑政策

现场笔记:最能抓 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 一个已注册用户正在登录页面
      ^ 前置条件:用户已存在,页面已加载

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 或发布评审里使用,而不是只当背景文章阅读。

旗舰页使用路径:
- 在计划或评审时打开本文。
- 把对应产物复制到工单或 PR。
- 用自己的系统、责任人和失败模式替换示例值。
- 如果证据行仍为空,就不要进入实现。

二次审阅记录:示例必须教会一种模式

这次检查的重点是:示例不能只是可复制文本,还要让读者学会一种测试模式,比如身份、状态、非法输入、时序、权限或重试。

示例复核:
- 保留能写明操作者、触发动作和可观测结果的示例。
- 一个锋利失败路径,胜过三个泛泛正常路径。
- 高风险场景补 replay、timeout、permission 或 partial success。
- 删除只是复述功能标题的标准。

编辑复核记录

复核日期:2026-04-29。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。

关键词:验收标准示例 · 验收标准模板 · given when then 示例 · 如何编写验收标准 · 用户故事验收标准示例

专题阅读路径

这篇文章归入验收标准主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。

交互式生成规格
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
试用规格生成器

编辑说明

本文讲解的是面向软件交付团队的验收标准编写方法。所有示例均为工程场景演示,不构成法律、税务或投资建议。