User Story Pack:用户故事的自动化
「2024年,一位敏捷教练崩溃地说:’我们的用户故事写得很标准——As a… I want… So that…,但开发出来的功能总是和需求有偏差。’问题不在于格式,而在于用户故事是’给人读的’,而不是’给机器执行的’。在AI时代,用户故事需要从自然语言描述进化为可自动验证的规格说明。」
📋 本文结构
- 传统用户故事的困境 — 为什么 As a… I want… 不够了
- 什么是 User Story Pack — 结构化用户故事包定义
- 三层结构:Story → Scenario → Example — 完整的层次化组织
- 从用户故事到自动化测试 — 无缝衔接 BDD
- AI 自动生成与验证 — 如何与 AI 协作
- 实战:电商系统的用户故事包 — 完整案例
- 写在最后:从故事到契约 — 范式转移的意义
一、传统用户故事的困境
经典用户故事格式
敏捷开发中,用户故事的标准格式是:
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:架构设计的机器可读化》