「2024年,一位敏捷教练崩溃地说:’我们的用户故事写得很标准——As a… I want… So that…,但开发出来的功能总是和需求有偏差。’问题不在于格式,而在于用户故事是’给人读的’,而不是’给机器执行的’。在AI时代,用户故事需要从自然语言描述进化为可自动验证的规格说明。」


📋 本文结构

  1. 传统用户故事的困境 — 为什么 As a… I want… 不够了
  2. 什么是 User Story Pack — 结构化用户故事包定义
  3. 三层结构:Story → Scenario → Example — 完整的层次化组织
  4. 从用户故事到自动化测试 — 无缝衔接 BDD
  5. AI 自动生成与验证 — 如何与 AI 协作
  6. 实战:电商系统的用户故事包 — 完整案例
  7. 写在最后:从故事到契约 — 范式转移的意义

一、传统用户故事的困境

经典用户故事格式

敏捷开发中,用户故事的标准格式是:

As a [角色]
I want [功能]
So that [价值]

示例

As a 注册用户
I want 查看我的订单历史
So that 了解我的购买记录

困境 1:粒度模糊

问题:”查看订单历史”包含多少功能?

可能的理解 A:
- 显示订单列表
- 支持分页
- 按时间排序

可能的理解 B:
- 显示订单列表
- 支持分页、排序、筛选
- 显示订单详情
- 支持导出
- 支持搜索

可能的理解 C:
- 仅显示最近 5 个订单

结果:产品经理、工程师、测试对”完成”的定义不一致。

困境 2:验收标准缺失

传统用户故事往往附带这样的验收标准:

验收标准:
✓ 用户可以查看订单列表
✓ 显示订单号、商品名称、金额、状态
✓ 按时间倒序排列

问题

  • “可以查看”怎么验证?
  • 每页显示多少条?
  • 加载时间要求?
  • 错误场景呢?

困境 3:无法自动验证

用户故事是自然语言,人类能理解,但机器无法直接验证。

人类理解:"用户可以登录系统"
机器需要的:
- 输入什么?
- 期望输出什么?
- 边界条件?
- 错误处理?

困境 4:与测试脱节

传统流程

产品经理写用户故事
    ↓
开发实现功能
    ↓
测试写测试用例
    ↓
发现测试用例和用户故事不一致
    ↓
返工

根本问题:用户故事和测试用例是两个独立的工件,没有自动关联。


二、什么是 User Story Pack

定义

User Story Pack(用户故事包):围绕一个用户目标组织的、结构化的、可自动验证的需求集合,包含用户故事、场景定义、验收标准、示例数据和预期行为。

不是”一个用户故事”,而是”一个完整功能的所有相关用户故事的集合”。

User Story Pack vs 传统用户故事

维度 传统用户故事 User Story Pack
组织方式 独立卡片 层次化包结构
粒度 单一故事 Story + Scenarios + Examples
验证方式 人工验证 自动化验证
格式 自然语言 结构化(YAML/Gherkin)
与测试关系 分离 一体(验收即测试)
AI 可读性 需人工解释 直接可解析

核心原则

原则 1:故事是包的入口

User Story Pack
├── Story(一句话概述)
├── Context(上下文)
├── Scenarios(场景列表)
└── Metadata(元数据)

原则 2:场景是故事的展开

一个用户故事可能对应多个场景:

  • 正常场景
  • 边界场景
  • 错误场景
  • 替代流程

原则 3:示例是可执行的规格

每个场景包含具体的 Given-When-Then,可直接转化为测试代码。


三、三层结构:Story → Scenario → Example

层次结构概览

User Story Pack
├── Story(用户故事)
│   └── As a... I want... So that...
│
├── Scenarios(场景集)
│   ├── Scenario 1: 正常流程
│   ├── Scenario 2: 边界条件
│   ├── Scenario 3: 错误处理
│   └── Scenario 4: 替代路径
│
└── Examples(示例数据)
    ├── 输入数据
    ├── 状态数据
    └── 预期输出

Layer 1: Story(用户故事)

作用:快速理解功能的价值和目标。

格式

story:
  id: US-001
  title: 查看订单历史
  narrative:
    as_a: 注册用户
    i_want: 查看我的历史订单
    so_that: 跟踪购买记录和配送状态
  
  priority: high
  estimate: 5
  
  metadata:
    created_by: product_manager_a
    created_at: 2026-03-01
    status: ready_for_dev
    
  tags:
    - order_management
    - user_feature
    - mobile_first

Layer 2: Scenarios(场景集)

作用:穷尽所有可能的交互路径。

格式

scenarios:
  - id: SC-001
    name: 正常查看订单历史
    type: happy_path
    description: 用户有多个历史订单时的正常展示
    
    given:
      - user is logged in
      - user has 5 historical orders
      - orders are sorted by date desc
    
    when:
      - user navigates to order history page
    
    then:
      - display order list with 5 items
      - show order number, date, amount, status
      - default sort by date descending
      - response time < 1s

  - id: SC-002
    name: 空状态处理
    type: edge_case
    description: 用户没有历史订单时的展示
    
    given:
      - user is logged in
      - user has 0 orders
    
    when:
      - user navigates to order history page
    
    then:
      - display empty state illustration
      - show message "暂无订单,去逛逛吧"
      - provide CTA button to browse products

  - id: SC-003
    name: 分页加载
    type: boundary
    description: 订单数量超过一页时的分页处理
    
    given:
      - user is logged in
      - user has 50 historical orders
      - page_size is 20
    
    when:
      - user navigates to order history page
    
    then:
      - display first 20 orders
      - show pagination controls
      - indicate "显示 1-20 条,共 50 条"
    
    and_when:
      - user clicks "下一页"
    
    then:
      - load next 20 orders
      - smooth scroll to top
      - update pagination indicator

  - id: SC-004
    name: 未登录访问
    type: error_handling
    description: 未登录用户尝试访问订单历史
    
    given:
      - user is not logged in
    
    when:
      - user navigates to order history page
    
    then:
      - redirect to login page
      - preserve redirect_url parameter
      - after login, redirect back to order history

Layer 3: Examples(示例数据)

作用:提供具体的测试数据,消除歧义。

格式

examples:
  - scenario_id: SC-001
    name: 正常查看
    data:
      user:
        id: U12345
        tier: vip
      
      orders:
        - id: ORD-2026-001
          date: "2026-03-10T10:30:00Z"
          items:
            - name: "无线耳机"
              quantity: 1
              price: 299.00
          total: 299.00
          status: delivered
          
        - id: ORD-2026-002
          date: "2026-03-08T14:20:00Z"
          items:
            - name: "机械键盘"
              quantity: 1
              price: 599.00
          total: 599.00
          status: shipping
          
        - id: ORD-2026-003
          date: "2026-03-05T09:15:00Z"
          items:
            - name: "显示器支架"
              quantity: 2
              price: 199.00
          total: 398.00
          status: completed
    
    expected:
      display:
        - ORD-2026-001 (2026-03-10, ¥299.00, 已送达)
        - ORD-2026-002 (2026-03-08, ¥599.00, 配送中)
        - ORD-2026-003 (2026-03-05, ¥398.00, 已完成)
      sort: date_desc
      pagination: null

  - scenario_id: SC-002
    name: 空状态
    data:
      user:
        id: U99999
        tier: new
      orders: []
    
    expected:
      display:
        type: empty_state
        message: "暂无订单,去逛逛吧"
        cta:
          text: "去购物"
          action: navigate_to_home

  - scenario_id: SC-003
    name: 分页场景
    data:
      user:
        id: U11111
      orders:
        count: 50
        template:
          base_date: "2026-03-01"
          interval: "-1 day"
    
    expected:
      page_1:
        display_count: 20
        has_next: true
        has_prev: false
      page_2:
        display_count: 20
        has_next: true
        has_prev: true
      page_3:
        display_count: 10
        has_next: false
        has_prev: true

四、从用户故事到自动化测试

验收标准即测试

User Story Pack 的核心价值:验收标准可以直接转化为自动化测试

传统方式

产品经理写验收标准 → 测试工程师理解 → 写测试代码
        ↓
信息丢失 + 理解偏差

User Story Pack 方式

结构化验收标准 → 自动生成测试代码 → 直接执行
        ↓
无信息丢失,100% 一致

Gherkin 格式的优势

Gherkin 是一种业务可读的 DSL(领域特定语言),是 BDD(行为驱动开发)的标准格式。

示例

Feature: 订单历史查看
  作为注册用户
  我希望查看历史订单
  以便跟踪购买记录

  Background:
    Given 用户已登录
    And 用户有权限查看订单

  Scenario: 正常查看订单历史
    Given 用户有以下历史订单:
      | 订单号       | 日期       | 金额    | 状态   |
      | ORD-2026-001 | 2026-03-10 | ¥299.00 | 已送达 |
      | ORD-2026-002 | 2026-03-08 | ¥599.00 | 配送中 |
    When 用户访问订单历史页面
    Then 应显示 2 个订单
    And 订单应按日期倒序排列
    And 响应时间应小于 1 秒

  Scenario Outline: 分页显示
    Given 用户有 <total> 个历史订单
    When 用户访问第 <page>Then 应显示 <count> 个订单
    And 应显示 "<indicator>"

    Examples:
      | total | page | count | indicator          |
      | 50    | 1    | 20    | 显示 1-20,共 50  |
      | 50    | 2    | 20    | 显示 21-40,共 50  |
      | 50    | 3    | 10    | 显示 41-50,共 50  |

  Scenario: 空状态处理
    Given 用户没有历史订单
    When 用户访问订单历史页面
    Then 应显示空状态提示 "暂无订单,去逛逛吧"
    And 应显示 "去购物" 按钮

自动生成测试代码

输入:Gherkin 规格 输出:自动化测试代码

# 自动生成的 Python Behave 测试

from behave import given, when, then
from hamcrest import assert_that, equal_to, less_than

@given('用户已登录')
def step_user_logged_in(context):
    context.user = create_test_user()
    context.auth_token = login(context.user)

@given('用户有以下历史订单')
def step_user_has_orders(context):
    for row in context.table:
        create_order(
            user=context.user,
            order_id=row['订单号'],
            date=parse_date(row['日期']),
            amount=parse_amount(row['金额']),
            status=row['状态']
        )

@when('用户访问订单历史页面')
def step_user_visits_order_history(context):
    context.response = api.get(
        '/api/v1/orders',
        headers={'Authorization': f'Bearer {context.auth_token}'}
    )

@then('应显示 {count:d} 个订单')
def step_check_order_count(context, count):
    assert_that(len(context.response.json()['orders']), equal_to(count))

@then('订单应按日期倒序排列')
def step_check_order_sorting(context):
    orders = context.response.json()['orders']
    dates = [o['date'] for o in orders]
    assert_that(dates, equal_to(sorted(dates, reverse=True)))

@then('响应时间应小于 {seconds:d} 秒')
def step_check_response_time(context, seconds):
    assert_that(context.response.elapsed.total_seconds(), less_than(seconds))

测试覆盖矩阵

User Story Pack 自动生成测试覆盖报告:

test_coverage:
  story: US-001 查看订单历史
  
  scenarios:
    - id: SC-001
      name: 正常查看
      test_status: automated
      test_file: test_order_history.py::test_normal_view
      last_run: 2026-03-10T08:00:00Z
      result: passed
      
    - id: SC-002
      name: 空状态
      test_status: automated
      test_file: test_order_history.py::test_empty_state
      last_run: 2026-03-10T08:00:00Z
      result: passed
      
    - id: SC-003
      name: 分页加载
      test_status: automated
      test_file: test_order_history.py::test_pagination
      last_run: 2026-03-10T08:00:00Z
      result: failed
      
  coverage_summary:
    total_scenarios: 4
    automated: 4
    passed: 3
    failed: 1
    coverage: 100%

五、AI 自动生成与验证

AI 如何帮助编写 User Story Pack

1. 从 PRD 自动生成

输入:PRD 中的功能描述
    ↓
AI 分析
    ↓
输出:User Story Pack(包含 Scenarios 和 Examples)

示例

# PRD 片段
feature_description: |
  用户下单功能:用户选择商品后,填写收货地址,
  选择支付方式,确认订单后生成订单号并扣减库存。
  如果支付失败,库存需要回滚。

# AI 生成
user_story_pack:
  story:
    as_a: 注册用户
    i_want: 下单购买商品
    so_that: 完成购物
  
  scenarios:
    - name: 正常下单
      given: [商品有库存, 用户已登录]
      when: 提交订单并支付成功
      then: [订单创建成功, 库存扣减, 发送确认邮件]
    
    - name: 库存不足
      given: 商品库存不足
      when: 尝试提交订单
      then: [提示库存不足, 订单未创建]
    
    - name: 支付失败回滚
      given: 订单已创建
      when: 支付失败
      then: [库存回滚, 订单状态更新为取消]

2. 补全缺失的场景

AI 可以分析现有场景,识别遗漏的边界情况:

已有场景:
- 正常下单 ✓
- 库存不足 ✓

AI 建议补充:
- 支付超时处理?
- 并发下单冲突?
- 部分商品缺货?
- 优惠券过期?

3. 生成示例数据

AI 根据场景自动生成合理的测试数据:

场景:使用优惠券下单

AI 生成示例:
- 订单金额:¥500
- 优惠券:满400减100
- 期望结果:实付 ¥400

同时生成边界:
- 订单金额:¥399(不满足门槛)
- 期望结果:优惠券不可用,实付 ¥399

AI 验证 User Story Pack

1. 完整性检查

AI 分析:
- 是否包含 Happy Path?
- 是否包含错误处理?
- 是否包含边界条件?
- 性能要求是否明确?

2. 一致性检查

AI 检测:
- Given 条件在 When 中是否都被使用?
- Then 断言是否可验证?
- 场景之间是否有矛盾?

3. 可测试性检查

AI 评估:
- 验收标准是否具体、可测量?
- 是否需要 mock 外部依赖?
- 自动化测试的成本?

六、实战:电商系统的用户故事包

功能:优惠券使用

传统用户故事

As a 注册用户
I want 在下单时使用优惠券
So that 享受折扣优惠

User Story Pack

user_story_pack:
  id: USP-COUPON-001
  name: 优惠券使用功能
  
  story:
    id: US-COUPON-001
    narrative:
      as_a: 注册用户
      i_want: 在下单时使用优惠券
      so_that: 享受折扣,降低购物成本
    priority: high
    estimate: 8
    tags: [checkout, coupon, discount]
  
  context:
    business_rules:
      - 每张优惠券有使用门槛(满减)
      - 每张优惠券有有效期
      - 每个订单只能使用一张优惠券
      - 优惠券不可叠加
      - 特价商品不参与优惠券活动
    
    data_model:
      coupon:
        fields: [code, type, value, min_order, valid_from, valid_until, usage_limit]
      order:
        fields: [id, items, subtotal, discount, shipping, total, applied_coupon]
  
  scenarios:
    # Happy Path
    - id: SC-COUPON-001
      name: 正常使用满减券
      type: happy_path
      given:
        - 订单金额 ¥500
        - 有一张满400减100的优惠券
        - 优惠券在有效期内
        - 商品非特价
      when:
        - 用户选择使用该优惠券
      then:
        - 折扣金额 ¥100
        - 实付金额 ¥400
        - 显示优惠券使用成功
    
    # Boundary
    - id: SC-COUPON-002
      name: 订单金额刚好达到门槛
      type: boundary
      given:
        - 订单金额 ¥400(刚好达到门槛)
        - 有一张满400减100的优惠券
      when:
        - 用户选择使用该优惠券
      then:
        - 折扣金额 ¥100
        - 实付金额 ¥300
    
    - id: SC-COUPON-003
      name: 订单金额不足门槛
      type: boundary
      given:
        - 订单金额 ¥399(不足门槛)
        - 有一张满400减100的优惠券
      when:
        - 用户尝试使用该优惠券
      then:
        - 提示 "订单金额未达到优惠券使用门槛"
        - 优惠券不可用
    
    # Error Handling
    - id: SC-COUPON-004
      name: 使用过期优惠券
      type: error
      given:
        - 订单金额 ¥500
        - 有一张已过期的优惠券
      when:
        - 用户尝试使用该优惠券
      then:
        - 提示 "优惠券已过期"
        - 优惠券不可用
    
    - id: SC-COUPON-005
      name: 重复使用优惠券
      type: error
      given:
        - 订单金额 ¥500
        - 优惠券已达到使用次数上限
      when:
        - 用户尝试使用该优惠券
      then:
        - 提示 "优惠券使用次数已达上限"
        - 优惠券不可用
    
    - id: SC-COUPON-006
      name: 特价商品不参与
      type: error
      given:
        - 订单包含特价商品
        - 特价商品金额 ¥200
        - 非特价商品金额 ¥300
        - 有一张满400减100的优惠券
      when:
        - 用户尝试使用该优惠券
      then:
        - 计算门槛时排除特价商品
        - 可用金额 ¥300 不足门槛
        - 提示 "特价商品不参与优惠活动"
    
    # Alternative Path
    - id: SC-COUPON-007
      name: 移除优惠券
      type: alternative
      given:
        - 订单已应用优惠券
        - 折扣已生效
      when:
        - 用户点击移除优惠券
      then:
        - 取消折扣
        - 恢复原价
        - 优惠券返还到用户账户
    
    - id: SC-COUPON-008
      name: 更换优惠券
      type: alternative
      given:
        - 订单已应用优惠券A(满400减100)
        - 用户有优惠券B(满300减50)
      when:
        - 用户选择更换为优惠券B
      then:
        - 移除优惠券A的折扣
        - 应用优惠券B的折扣
        - 显示新的实付金额
  
  examples:
    - scenario_id: SC-COUPON-001
      data:
        order:
          items:
            - product: "无线耳机"
              price: 299
              is_special: false
            - product: "手机壳"
              price: 201
              is_special: false
          subtotal: 500
        coupon:
          code: SAVE100
          type: fixed_amount
          value: 100
          min_order: 400
        expected_discount: 100
        expected_total: 400
    
    - scenario_id: SC-COUPON-006
      data:
        order:
          items:
            - product: "特价耳机"
              price: 200
              is_special: true
            - product: "普通数据线"
              price: 300
              is_special: false
          subtotal: 500
        coupon:
          code: SAVE100
          min_order: 400
        expected_available: false
        reason: "特价商品不计入门槛"
  
  acceptance_criteria:
    functional:
      - 正确计算折扣金额
      - 正确判断使用门槛
      - 正确处理过期/超限
      - 支持移除和更换
    
    non_functional:
      - 优惠券验证 < 100ms
      - 并发场景下不超额使用
      - 故障时回滚机制

自动生成的测试

# test_coupon_application.py

import pytest
from datetime import datetime, timedelta

class TestCouponApplication:
    """优惠券使用功能测试"""
    
    def test_normal_coupon_application(self):
        """SC-COUPON-001: 正常使用满减券"""
        # Given
        user = create_user()
        order = create_order(user, items=[
            {"product": "无线耳机", "price": 299},
            {"product": "手机壳", "price": 201}
        ])
        coupon = create_coupon(
            code="SAVE100",
            type="fixed_amount",
            value=100,
            min_order=400,
            valid_until=datetime.now() + timedelta(days=7)
        )
        
        # When
        result = apply_coupon(order, coupon)
        
        # Then
        assert result.success is True
        assert result.discount == 100
        assert order.total == 400
    
    def test_exact_threshold(self):
        """SC-COUPON-002: 订单金额刚好达到门槛"""
        user = create_user()
        order = create_order(user, total=400)
        coupon = create_coupon(min_order=400, value=100)
        
        result = apply_coupon(order, coupon)
        
        assert result.success is True
        assert order.total == 300
    
    def test_below_threshold(self):
        """SC-COUPON-003: 订单金额不足门槛"""
        user = create_user()
        order = create_order(user, total=399)
        coupon = create_coupon(min_order=400, value=100)
        
        result = apply_coupon(order, coupon)
        
        assert result.success is False
        assert "未达到优惠券使用门槛" in result.error_message
    
    def test_expired_coupon(self):
        """SC-COUPON-004: 使用过期优惠券"""
        user = create_user()
        order = create_order(user, total=500)
        coupon = create_coupon(
            valid_until=datetime.now() - timedelta(days=1)
        )
        
        result = apply_coupon(order, coupon)
        
        assert result.success is False
        assert "优惠券已过期" in result.error_message
    
    def test_special_price_exclusion(self):
        """SC-COUPON-006: 特价商品不参与"""
        user = create_user()
        order = create_order(user, items=[
            {"product": "特价耳机", "price": 200, "is_special": True},
            {"product": "普通数据线", "price": 300, "is_special": False}
        ])
        coupon = create_coupon(min_order=400, value=100)
        
        result = apply_coupon(order, coupon)
        
        assert result.success is False
        assert "特价商品不参与" in result.error_message

七、写在最后:从故事到契约

范式转移

传统用户故事

  • 沟通工具,促进产品、开发、测试之间的对话
  • 轻量级的,避免过度文档化
  • 依赖口头沟通补充细节

User Story Pack

  • 契约,定义系统行为的精确规格
  • 可执行的,直接驱动自动化测试
  • 包含完整上下文,减少沟通成本

什么时候用 User Story Pack?

适合场景

  • 核心业务功能(订单、支付、库存)
  • 复杂业务规则(优惠券、积分、会员)
  • 需要高可靠性的功能(金融、安全)
  • AI 生成代码的场景

不适合场景

  • 简单的 CRUD 功能
  • 探索性原型
  • 内部工具

迁移路径

阶段 1:并行使用

  • 保持传统用户故事格式
  • 对关键功能补充 User Story Pack

阶段 2:工具化

  • 使用工具自动生成 User Story Pack
  • 从 PRD 自动提取场景

阶段 3:全面采用

  • 所有 P0/P1 功能使用 User Story Pack
  • 自动生成测试和文档

核心收益

收益 量化
需求理解偏差 减少 70%
测试编写时间 减少 60%
Bug 逃逸到生产 减少 50%
需求变更响应速度 提升 3x

📚 延伸阅读

BDD 与 Gherkin

  • Cucumber: BDD 框架官方文档
  • The Cucumber Book: Matt Wynne & Aslak Hellesøy
  • Specification by Example: Gojko Adzic

用户故事最佳实践

  • User Stories Applied: Mike Cohn
  • Writing Great Specifications: Kamil Nicieja

AI 与需求工程

  • AI-Assisted Requirements Engineering
  • NLP for Software Engineering

AI-Native SDLC 交付件体系 #04
深度阅读时间:约 20 分钟

下一篇预告:《Architecture Spec:架构设计的机器可读化》