小厂初级程序员如何理解敏捷开发:从写好用户故事开始

适合读者: 经常拿到一句话需求就开始编码,却在开发中途反复返工的初级程序员。
本文不比较“大厂”和“小厂”谁的流程更好,也不主张照搬复杂仪式。它只解决一个具体问题:怎样把一句模糊需求变成团队可以讨论、开发和验收的小增量。
先给结论
- 用户故事描述的是用户目标和业务价值,不是接口、表结构或页面清单。
- 一张故事卡不是完整需求;真正的信息来自围绕它展开的对话、示例和验收标准。
- 验收标准应描述可观察的结果。数据库表、类名和框架配置通常属于后续技术任务。
- Given-When-Then 用来表达上下文、动作和结果,不等于必须引入 Cucumber。
- 开发前至少确认角色、目标、价值、业务规则、异常路径、非目标和完成条件。
贯穿全文的例子是“失败任务重新提交”。你可以把它替换成退款、订单取消或消息重试,用同样的方法澄清需求。
四类产物不要混在一起
| 产物 | 回答的问题 | 示例 |
|---|---|---|
| 用户故事 | 谁要解决什么问题,为什么有价值? | 运营人员重新提交失败任务,以减少人工排查时间 |
| 验收标准 | 什么结果出现时,业务可以接受? | 只有失败状态允许重试;成功后生成新的处理记录 |
| 技术任务 | 工程上需要完成什么? | 新增重试接口、幂等键和消息消费者 |
| 自动化测试 | 怎样持续证明行为没有退化? | API 测试、领域规则测试、消息消费测试 |
很多小厂初级程序员在接需求时,都会遇到一种很熟悉的状态:
产品:“这里加一个任务重试功能。”
研发:“好的。”
然后开发人员开始建表、写接口、改页面、调用第三方平台、增加队列和消费者。
做到一半才发现:
- 哪些状态允许重试?
- 重试后状态应该变成什么?
- 失败原因是否保留?
- 重复点击怎样处理?
- 消息重复消费怎样处理?
- 第三方平台仍然失败时,任务状态怎样变化?
于是功能越做越乱,沟通越来越低效,代码越改越心虚。
很多时候,初级程序员真正痛苦的不是“代码不会写”,而是:
不知道需求到底该怎么理解、怎么拆、怎么问、怎么确认。
所以这篇文章想从一个小厂初级程序员的视角,重新理解大厂敏捷开发里一个非常基础但非常重要的概念:
用户故事。
用户故事到底是什么
用户故事不是“需求描述得短一点”,也不是“开发任务换个说法”。
用户故事的核心是:
从用户视角描述一个有价值的业务目标。
经典格式是:
作为一个【用户角色】,
我希望【完成某个目标/行为】,
以便【获得某个业务价值】。
例如:
作为一个内容运营人员,
我希望可以按任务状态筛选 AI 生成任务,
以便快速找到失败任务并重新处理。
这句话里面有三个关键点:
谁需要?
他想完成什么?
为什么这件事有价值?
如果一个需求说不清楚这三个问题,那么这个需求很可能还没有被真正理解。
用户故事不是技术任务
很多研发容易把用户故事写成这样:
作为一个开发人员,
我希望新增一个 task_status 字段,
以便前端可以查询任务状态。
这看起来像用户故事,但其实不是。
它的问题是:
角色不是真实业务用户
目标是技术实现
价值也不清晰
“新增 task_status 字段”是技术任务,不是用户故事。
更好的写法应该是:
作为一个内容运营人员,
我希望可以看到每个 AI 任务的当前处理状态,
以便判断任务是生成中、已完成、失败还是需要重试。
然后再从这个用户故事下面拆出技术任务:
- 新增任务状态字段
- 定义任务状态枚举
- 修改任务查询接口
- 前端展示任务状态标签
- 增加失败任务筛选条件
- 补充任务状态相关测试
也就是说:
用户故事描述业务价值
技术任务描述实现方式
如果一开始就把技术任务伪装成用户故事,后面就很容易出现一个问题:
代码写了很多,但不知道到底解决了谁的什么问题。
为什么小团队更需要用户故事
很多人觉得用户故事、敏捷开发、验收标准这些东西是“大厂流程”,小厂没必要这么麻烦。
但我的理解恰恰相反:
小厂更需要用简单清晰的方式降低沟通成本。
因为小厂往往有这些问题:
产品文档不完整
需求变化快
研发人数少
测试不充分
开发经常要自己理解业务
很多规则都靠口头沟通
如果没有用户故事,需求就很容易变成:
产品:做一个任务列表。
研发:好的。
然后研发开始想:
任务列表展示哪些字段?
能不能筛选?
能不能分页?
失败任务怎么处理?
有没有权限控制?
任务状态有哪些?
删除任务是真删除还是逻辑删除?
这些问题如果不提前问清楚,最后一定会在开发中、联调中、测试中、上线后暴露出来。
用户故事的价值不是增加流程,而是帮助我们提前把问题说清楚。
用 INVEST 检查故事质量
判断用户故事质量,可以使用 INVEST 原则。
| 原则 | 含义 | 理解方式 |
|---|---|---|
| Independent | 独立 | 尽量可以单独开发、测试、上线 |
| Negotiable | 可讨论 | 它不是死合同,而是沟通起点 |
| Valuable | 有价值 | 对用户或业务有明确价值 |
| Estimable | 可估算 | 团队能大致判断工作量 |
| Small | 足够小 | 一个迭代内能完成 |
| Testable | 可测试 | 有明确验收标准 |
对初级研发来说,不需要一开始就把理论背得很熟。
先记住三个最重要的标准:
有明确用户
有业务价值
可以被验收
如果一个用户故事没有真实用户,它就可能只是系统功能。 如果没有业务价值,它就可能只是技术任务。 如果不能验收,它就很容易导致扯皮。
从模板开始,但不要止于模板
实际工作中,不建议只写一句:
作为谁,我想做什么,以便什么。
这一句太短,适合做标题或概要,但不够支撑开发。
更推荐使用下面这个模板:
标题:
【角色】可以【完成某个业务目标】
用户故事:
作为一个【具体用户角色】,
我希望【完成某个业务行为】,
以便【获得某个业务价值】。
业务背景:
为什么现在需要这个能力?
当前用户遇到了什么问题?
如果不做,会有什么影响?
验收标准:
1. 给定【某个前置条件】,当【用户执行某个操作】,那么【系统产生某个结果】
2. 给定【某个异常情况】,当【用户执行某个操作】,那么【系统给出明确反馈】
3. 给定【某个边界条件】,当【用户执行某个操作】,那么【系统应该如何限制】
非目标:
本故事不包含哪些内容,避免范围失控。
补充说明:
字段、权限、页面入口、状态流转、数据规则、异常处理等。
这个模板的好处是:
产品可以表达业务价值
研发可以理解开发边界
测试可以明确验收标准
团队可以提前发现歧义
四种常见错误写法
把技术实现当成用户故事
错误示例:
作为系统,
我希望新增 Redis 缓存,
以便提高接口性能。
这个故事的问题很明显:
“系统”不是用户
“新增 Redis 缓存”是实现方式
“提高性能”没有具体业务场景
更好的写法是:
作为一个内容运营人员,
我希望打开任务列表时可以快速看到最新任务,
以便在任务高峰期及时处理异常任务。
然后技术任务再考虑:
是否需要 Redis 缓存
是否需要分页优化
是否需要数据库索引
是否需要异步加载
是否需要接口限流
用户故事不要直接绑定技术方案。
因为技术方案是可以讨论和替换的,而用户价值才是需求的核心。
把页面当成用户故事
错误示例:
作为用户,
我希望有一个任务列表页面。
这句话看似没错,但其实价值不清楚。
“有一个页面”不是业务价值,用户通过页面完成什么才是价值。
更好的写法是:
作为一个内容运营人员,
我希望可以查看 AI 任务列表,
以便了解每个任务的处理进度和结果状态。
这样研发才能进一步追问:
任务列表需要展示哪些状态?
是否需要分页?
是否需要搜索?
是否需要按状态筛选?
失败任务是否需要显示失败原因?
不同角色能看到的数据是否一样?
一个好的用户故事,应该能引出正确的问题。
角色定义过于宽泛
错误示例:
作为用户,
我希望导出数据,
以便使用数据。
这个故事的问题是角色太模糊,价值也太模糊。
谁是用户? 导出什么数据? 导出后给谁用? 为什么要导出?
更好的写法是:
作为一个数据运营人员,
我希望可以导出筛选后的达人数据,
以便交给商务团队进行合作邀约。
这样边界就清楚很多:
角色:数据运营人员
目标:导出筛选后的达人数据
价值:给商务团队做合作邀约
角色越具体,需求边界越清晰。
故事大到无法快速反馈
错误示例:
作为管理员,
我希望管理所有用户权限,
以便控制系统访问。
这个故事太大了。
“管理所有用户权限”里面可能包含:
创建角色
编辑角色
删除角色
给角色分配权限
给用户分配角色
禁用用户账号
查看权限变更记录
控制菜单权限
控制按钮权限
控制数据权限
这种故事无法准确估算,也很难一次性开发完成。
应该拆成多个小故事:
作为管理员,
我希望可以创建角色,
以便按岗位管理权限集合。
作为管理员,
我希望可以给角色分配菜单权限,
以便控制不同岗位可以访问的页面。
作为管理员,
我希望可以给用户分配角色,
以便控制用户可以使用的系统功能。
作为管理员,
我希望可以禁用用户账号,
以便阻止离职或异常账号继续访问系统。
好的用户故事应该足够小,小到可以开发、测试、验收。
按业务价值拆分,而不是按技术层
很多研发习惯按技术层拆任务,于是会写成:
用户故事 1:新增任务表
用户故事 2:新增任务接口
用户故事 3:新增任务页面
用户故事 4:新增任务查询 SQL
这不是用户故事,这是开发任务拆分。
用户故事应该按用户价值拆:
用户故事 1:运营人员可以创建 AI 任务
用户故事 2:运营人员可以查看任务处理状态
用户故事 3:运营人员可以查看任务生成结果
用户故事 4:运营人员可以重新提交失败任务
用户故事 5:运营人员可以按状态筛选任务
然后每个用户故事下面再拆技术任务。
正确结构应该是:
Epic:业务大目标
Feature:功能能力
User Story:用户价值切片
Task:技术实现任务
例如:
Epic:AI 任务管理平台
Feature:任务生命周期管理
User Story:运营人员可以创建 AI 任务
Task:设计任务提交接口
Task:实现任务参数校验
Task:保存任务记录
Task:调用三方模型平台
Task:前端创建任务表单
Task:补充自动化测试
这才是比较清晰的拆分方式。
把验收标准写成可观察结果
用户故事写完之后,最重要的是写验收标准。
因为用户故事描述的是目标,而验收标准描述的是:
做到什么程度才算完成?
不要这样写验收标准:
- 可以创建任务
- 可以查看任务
- 页面正常
这太模糊,无法测试,也无法避免扯皮。
更好的方式是使用 Given / When / Then 思维:
给定【前置条件】,
当【用户执行某个操作】,
那么【系统应该产生某个结果】。
例如:
验收标准 1:创建任务成功
给定运营人员已经登录系统,
并且填写了合法的任务参数,
当运营人员点击“提交任务”,
那么系统应创建一条任务记录,
并且任务状态应为“提交中”,
并且页面应提示“任务提交成功”。
再补充异常情况:
验收标准 2:缺少必填参数
给定运营人员已经登录系统,
并且没有填写提示词,
当运营人员点击“提交任务”,
那么系统不应创建任务,
并且页面应提示“提示词不能为空”。
再补充外部系统失败:
验收标准 3:三方平台提交失败
给定运营人员已经提交了合法任务参数,
并且三方模型平台返回失败,
当系统保存处理结果,
那么任务状态应变为“提交失败”,
并且系统应记录失败原因,
并且运营人员可以在任务详情中查看失败原因。
一个好的验收标准,应该能让产品、研发、测试对“完成”有同一个理解。
BDD 应该从讨论阶段开始
这里有一个容易误解的点。
有人会把开发顺序理解成:
用户目标
↓
业务规则
↓
验收标准
↓
领域模型
↓
接口设计
↓
数据设计
↓
代码实现
↓
自动化测试
这个顺序容易让人误解成:
先写完代码,最后才想测试。
这不是 BDD 的思想。
更准确地说,应该区分三件事:
验收标准 / 测试场景设计:应该在代码之前
自动化测试代码:可以在实现之前或实现过程中写
自动化测试执行 / 回归验证:通常在实现之后持续执行
所以更合理的顺序应该是:
用户目标
↓
业务规则
↓
验收标准
↓
BDD 场景 / 测试用例设计
↓
领域模型
↓
接口设计
↓
数据设计
↓
代码实现
↓
自动化测试执行 / 回归验证
BDD 的核心不是“代码写完后补测试”,而是:
先用业务语言描述期望行为,
再用这些行为驱动代码实现。
也就是说:
测试思考在前
测试执行贯穿始终
回归验证在后
用户故事、验收标准与 BDD 的关系
如果团队使用 BDD、Cucumber、Gherkin,那么用户故事和测试之间可以形成一条完整链路:
用户故事:描述用户需要什么价值
验收标准:描述如何判断价值是否实现
Gherkin 场景:把验收标准变成可执行示例
Step Definition:把场景连接到测试代码
生产代码:实现功能并通过测试
例如用户故事:
作为一个内容运营人员,
我希望可以重新提交失败的 AI 任务,
以便减少重复录入任务参数的成本。
对应的 Gherkin 场景可以是:
场景: 重新提交失败任务成功
假如 存在一个状态为“提交失败”的 AI 任务
当 运营人员重新提交该任务
那么 系统应受理本次重试请求
并且 任务状态变为“重试中”
并且 运营人员看到提示“任务已重新提交”
注意:
一个用户故事不一定只对应一个场景。
一个用户故事通常会对应多个场景:
成功场景
失败场景
权限场景
边界场景
异常场景
这才是真正有价值的 BDD。
Gherkin 不应该塞满技术细节
例如这个场景:
场景: 重新提交失败任务成功
假如 存在一个状态为“提交失败”的 AI 任务
当 运营人员点击“重新提交”
那么 系统会基于原任务参数重新提交任务
并且 任务状态变为“重试中”
并且 页面提示“任务已重新提交”
乍一看没有问题。
但如果从后端实现角度看,真实流程可能是:
运营人员发起重试
↓
后端校验任务是否允许重试
↓
任务状态改为“重试中”
↓
生成一条待执行任务
↓
发布任务重试消息
↓
消费者异步消费
↓
消费者调用三方模型平台
↓
三方平台返回 platformTaskId
↓
任务状态改为“生成中”
于是研发会开始纠结:
Gherkin 里面要不要写队列?
要不要写消费者?
要不要写 platformTaskId?
要不要写数据库记录?
要不要写 MQ 消息?
Step 里面到底是调用接口,还是直接调用消费者?
这时候一定要记住一句话:
用户故事 BDD 描述业务可观察行为,不描述技术实现细节。
用户故事场景里通常不要写:
那么 系统会发送一条 RabbitMQ 消息
并且 消费者会消费该消息
并且 调用 ModelPlatformGateway
并且 更新 task_retry_record 表
这些不是业务语言,而是实现语言。
业务人员关心的是:
我发起了重试,系统有没有受理?
这个任务现在是什么状态?
我能不能看到明确反馈?
失败任务有没有进入重新处理流程?
队列、消费者、网关、数据库这些后端细节,应该放到更低层的技术验收场景里。
区分业务验收与技术验证
对于后端程序员来说,一定要区分:
用户故事验收场景
后端技术验收场景
用户故事验收场景关注:
用户做了什么,系统给用户什么业务结果。
后端技术验收场景关注:
某个后端边界收到输入后,系统状态、返回结果、外部调用、副作用是否正确。
比如:
接口是否正确校验状态
状态是否正确流转
是否创建待执行任务
是否发布重试消息
消费者是否正确消费消息
消费者失败后是否记录失败原因
重复消费是否幂等
事务失败是否回滚
三方平台异常是否被正确处理
后端技术验收场景可以写技术细节,但要注意:
写的是后端可观察行为,不是代码执行过程。
先确定测试边界
写后端技术验收场景前,第一件事不是写 Given / When / Then,而是先问:
我这条场景到底要验哪个后端边界?
常见边界有这些:
| 测试边界 | 触发方式 | 主要验证 |
|---|---|---|
| HTTP 接口 | 调用 REST API | 状态码、响应体、数据库状态、副作用 |
| Application UseCase | 调用应用服务 | 业务流程编排、状态流转、发布事件 |
| Domain Model | 调用领域对象方法 | 业务规则、状态机、异常规则 |
| Consumer | 投递或消费消息 | 消息处理、幂等、失败处理 |
| Gateway Adapter | 调用三方适配器 | 请求转换、响应解析、异常映射 |
| Repository | 保存或查询实体 | 持久化映射、查询条件、事务行为 |
混乱通常来自没有先确定测试边界。
比如你写“重新提交失败任务成功”,但脑子里同时想验证:
HTTP 接口
UseCase
状态流转
队列发布
消费者
三方平台
数据库
页面提示
那一定会乱。
正确做法是:
一条技术验收场景,只验证一个主要边界。
技术验收场景模板
后端技术验收场景可以用这个结构:
场景: 【某个后端边界】在【某种条件】下应【产生某个技术/业务结果】
假如 【系统已有数据状态】
并且 【外部依赖行为】
当 【触发某个后端入口】
那么 【返回结果正确】
并且 【核心状态正确】
并且 【必要副作用正确】
对应到后端,就是:
Given:准备数据库状态、领域对象状态、外部依赖 Fake 行为
When:调用接口 / 调用 UseCase / 消费消息 / 调用适配器
Then:验证响应、数据库、消息、外部调用、状态流转
后端验收场景示例
接口层技术验收
接口层关注的是:
请求进来后,接口是否返回正确结果,并触发正确的应用行为。
例如:
@api @task-retry
功能: AI 任务重试接口
场景: 失败任务可以发起重试
假如 数据库中存在一个状态为“提交失败”的 AI 任务
当 客户端请求重新提交该任务
那么 接口响应状态码应为 200
并且 响应结果提示“任务已重新提交”
并且 该任务状态应变为“重试中”
并且 系统应生成一条待处理的重试任务
这里可以出现“接口”“状态码”“数据库”“待处理任务”。
因为这是后端技术验收场景,不是产品级用户故事场景。
但是不建议写成:
并且 RetryTaskController 调用了 RetryTaskUseCase.retry 方法
这个太贴代码实现了。
应该验证结果,而不是验证某个方法是否被调用。
好的表达是:
并且 系统应生成一条待处理的重试任务
而不是:
并且 调用了 retryTaskQueuePublisher.publish()
UseCase 层技术验收
UseCase 层关注:
应用流程是否编排正确。
例如:
@usecase @task-retry
功能: AI 任务重试用例
场景: 重试失败任务时创建待处理任务
假如 存在一个状态为“提交失败”的 AI 任务
当 任务重试用例处理该任务的重试请求
那么 任务状态应变为“重试中”
并且 系统应生成一条包含原任务参数的待处理任务
并且 系统应记录本次重试操作时间
这个场景不关心 HTTP,不关心 Controller,也不关心页面提示。
它只关心:
UseCase 收到重试请求后,
是否正确改变业务状态,
是否创建待处理任务,
是否记录必要信息。
Step Definition 可以直接调用:
retryTaskUseCase.retry(taskId, operatorId);
然后断言:
assertThat(taskRepository.findByTaskId(taskId).getStatus())
.isEqualTo(TaskStatus.RETRYING);
assertThat(fakeRetryTaskQueuePublisher.wasPublished(taskId))
.isTrue();
Step 的职责是准备数据、触发入口、验证结果,不要在 Step 里重新写一遍业务逻辑。
领域规则测试
领域模型层不一定要用 Cucumber。
很多时候 JUnit 更合适。
例如任务重试规则:
提交失败的任务允许重试
生成失败的任务允许重试
重试中的任务不允许重试
生成中的任务不允许重试
已完成的任务不允许重试
已取消的任务不允许重试
超过最大重试次数的任务不允许重试
如果用 Gherkin,可以这样写:
@domain @task-retry
功能: AI 任务重试规则
场景大纲: 只有指定状态的任务允许重试
假如 一个 AI 任务当前状态为“<当前状态>”
当 判断该任务是否允许重试
那么 结果应为“<是否允许>”
例子:
| 当前状态 | 是否允许 |
| 提交失败 | 是 |
| 生成失败 | 是 |
| 重试中 | 否 |
| 生成中 | 否 |
| 已完成 | 否 |
| 已取消 | 否 |
如果用 JUnit,可以更直接:
@Test
void submittedFailedTaskCanRetry() {
Task task = Task.failed();
assertThat(task.canRetry()).isTrue();
}
我的建议是:
复杂业务规则:可以用 Gherkin 表达
简单领域规则:JUnit 更直接
消费者技术验收
消费者层关注:
收到消息后,是否正确处理后台任务。
例如:
@consumer @task-retry
功能: AI 任务重试消费者
场景: 消费者成功提交重试任务到模型平台
假如 数据库中存在一个状态为“重试中”的 AI 任务
并且 存在一条待处理的重试任务
并且 模型平台接收任务成功并返回平台任务 ID “platform-001”
当 重试任务消费者处理该任务
那么 系统应使用原任务参数提交模型平台
并且 该任务状态应变为“生成中”
并且 该任务的平台任务 ID 应为“platform-001”
这个场景里可以写“消费者”“待处理任务”“模型平台”。
因为这就是消费者的技术验收。
但仍然不要写过度代码化的东西。
不推荐:
那么 executeRetryTask 方法被调用一次
并且 task_info 表的 platform_task_id 字段更新为 platform-001
推荐:
那么 系统应使用原任务参数提交模型平台
并且 该任务的平台任务 ID 应为“platform-001”
区别在于:
前者绑定代码实现和表结构
后者描述组件行为和系统结果
消费者失败场景
后端技术验收一定要重视失败场景。
场景: 模型平台提交失败时记录失败原因
假如 数据库中存在一个状态为“重试中”的 AI 任务
并且 存在一条待处理的重试任务
并且 模型平台提交失败,失败原因为“平台限流”
当 重试任务消费者处理该任务
那么 该任务状态应变为“提交失败”
并且 该任务失败原因应为“平台限流”
这个场景验证的是:
外部系统失败时,
后端状态和错误记录是否正确。
幂等技术验收
这是后端非常重要,但用户故事里一般不会直接写的内容。
场景: 重试任务被重复消费时不重复提交
假如 数据库中存在一个状态为“生成中”的 AI 任务
并且 该任务的平台任务 ID 为“platform-001”
并且 存在一条已处理过的重试任务
当 重试任务消费者再次处理该任务
那么 系统不应再次提交任务到模型平台
并且 该任务状态仍应为“生成中”
并且 该任务的平台任务 ID 仍应为“platform-001”
用户不关心“消息重复消费”,但后端必须关心。
所以它不一定出现在用户故事验收场景里,但应该出现在后端技术验收场景里。
事务一致性技术验收
如果系统里有数据库状态更新和消息发布,就要考虑一致性。
例如:
场景: 发布重试消息失败时不应改变任务状态
假如 存在一个状态为“提交失败”的 AI 任务
并且 重试消息发布会失败
当 任务重试用例处理该任务的重试请求
那么 本次重试请求应失败
并且 任务状态仍应保持为“提交失败”
并且 系统不应记录成功的重试操作
如果真实项目使用本地消息表或 Outbox Pattern,可以改成:
场景: 发起重试时写入本地消息表
假如 存在一个状态为“提交失败”的 AI 任务
当 任务重试用例处理该任务的重试请求
那么 任务状态应变为“重试中”
并且 系统应写入一条待发布的本地重试消息
这里验证的是架构决策:
状态变更和消息记录在同一个事务中完成。
技术场景的措辞边界
后端技术验收场景不是用户故事场景,所以可以写一些技术词。
可以写
HTTP 接口
状态码
请求参数
响应体
消息
消费者
任务状态
平台任务 ID
失败原因
幂等
事务
重试次数
本地消息表
外部平台返回失败
因为这些是后端行为的一部分。
少写或不写
Controller 调用了哪个 Service
Service 调用了哪个 Mapper
使用了哪个具体方法名
某个 private 方法执行了几次
某个 for 循环执行了几次
具体 SQL 是什么
具体表字段名是什么
除非你就是在做非常底层的持久化测试,否则不要把 Gherkin 写成代码执行轨迹。
Step Definition 的实现原则
技术验收场景的 Step Definition 有三个职责:
准备数据
触发后端入口
验证结果
不要在 Step 里写业务逻辑。
接口层 Step
@当("客户端请求重新提交该任务")
public void 客户端请求重新提交该任务() throws Exception {
response = mockMvc.perform(post("/api/tasks/{taskId}/retry", taskId)
.header("Authorization", token));
}
UseCase 层 Step
@当("任务重试用例处理该任务的重试请求")
public void 任务重试用例处理该任务的重试请求() {
result = retryTaskUseCase.retry(taskId, operatorId);
}
Consumer 层 Step
@当("重试任务消费者处理该任务")
public void 重试任务消费者处理该任务() {
retryTaskConsumer.consume(retryMessage);
}
验证状态
@那么("该任务状态应变为“{word}”")
public void 该任务状态应变为(String expectedStatus) {
Task task = taskRepository.findByTaskId(taskId);
assertThat(task.getStatus()).isEqualTo(TaskStatus.from(expectedStatus));
}
验证外部调用
@那么("系统应使用原任务参数提交模型平台")
public void 系统应使用原任务参数提交模型平台() {
assertThat(fakeModelPlatformGateway.wasSubmitted(taskId)).isTrue();
}
重点是:
Step 调用真实入口
Fake 外部依赖
断言系统结果
不要让 Step Definition 变成第二套业务代码。
组织后端 Feature 文件
后端项目里,不建议所有场景都放在一个 .feature 文件里。
可以按测试边界拆:
src/test/resources/features/
task-retry/
task-retry-api.feature
task-retry-usecase.feature
task-retry-consumer.feature
task-retry-policy.feature
model-platform-adapter.feature
也可以按业务模块拆:
src/test/resources/features/
ai-task/
retry-task.feature
retry-task-consumer.feature
task-status-policy.feature
标签可以这样加:
@api @task-retry
功能: 任务重试接口
@consumer @task-retry
功能: 重试任务消费者
@domain @task-retry
功能: 任务重试规则
这样可以分组运行:
只跑接口验收
只跑消费者测试
只跑领域规则测试
只跑完整回归
完整示例:失败任务重新提交
下面用一个完整案例说明如何从用户故事拆到后端技术验收。
1. 用户故事
标题:
运营人员可以重新提交失败的 AI 生成任务
用户故事:
作为一个内容运营人员,
我希望可以重新提交失败的 AI 生成任务,
以便在三方模型平台异常或参数可复用的情况下,不需要重新录入任务信息。
2. 业务背景
当前 AI 任务提交到三方模型平台后,可能因为网络异常、平台限流、模型服务异常等原因失败。
如果每次失败都要求运营人员重新创建任务,会增加重复操作成本,也容易导致参数录入错误。
因此系统需要支持对失败任务进行重新提交。
3. 用户故事验收场景
功能: AI 任务重试
场景: 运营人员可以重新提交失败任务
假如 存在一个状态为“提交失败”的 AI 任务
当 运营人员重新提交该任务
那么 系统应受理本次重试请求
并且 任务状态变为“重试中”
并且 运营人员看到提示“任务已重新提交”
场景: 生成中的任务不允许重新提交
假如 存在一个状态为“生成中”的 AI 任务
当 运营人员尝试重新提交该任务
那么 系统应拒绝本次操作
并且 提示“当前任务状态不允许重新提交”
4. 接口技术验收场景
@api @task-retry
功能: AI 任务重试接口
场景: 失败任务可以发起重试
假如 数据库中存在一个状态为“提交失败”的 AI 任务
当 客户端请求重新提交该任务
那么 接口响应状态码应为 200
并且 响应结果提示“任务已重新提交”
并且 该任务状态应变为“重试中”
并且 系统应生成一条待处理的重试任务
5. 非法状态技术验收场景
场景大纲: 非失败状态的任务不允许发起重试
假如 数据库中存在一个状态为“<任务状态>”的 AI 任务
当 客户端请求重新提交该任务
那么 接口响应状态码应为 400
并且 响应结果提示“当前任务状态不允许重新提交”
并且 该任务状态仍应为“<任务状态>”
例子:
| 任务状态 |
| 生成中 |
| 已完成 |
| 已取消 |
6. 消费者技术验收场景
@consumer @task-retry
功能: AI 任务重试消费者
场景: 消费者成功提交重试任务
假如 数据库中存在一个状态为“重试中”的 AI 任务
并且 存在一条待处理的重试任务
并且 模型平台接收任务成功并返回平台任务 ID “platform-001”
当 重试任务消费者处理该任务
那么 系统应使用原任务参数提交模型平台
并且 该任务状态应变为“生成中”
并且 该任务的平台任务 ID 应为“platform-001”
7. 消费者失败技术验收场景
场景: 模型平台提交失败时任务回到提交失败
假如 数据库中存在一个状态为“重试中”的 AI 任务
并且 存在一条待处理的重试任务
并且 模型平台提交失败,失败原因为“平台限流”
当 重试任务消费者处理该任务
那么 该任务状态应变为“提交失败”
并且 该任务失败原因应为“平台限流”
8. 幂等技术验收场景
场景: 重试任务被重复消费时不重复提交
假如 数据库中存在一个状态为“生成中”的 AI 任务
并且 该任务的平台任务 ID 为“platform-001”
并且 存在一条已处理过的重试任务
当 重试任务消费者再次处理该任务
那么 系统不应再次提交任务到模型平台
并且 该任务状态仍应为“生成中”
并且 该任务的平台任务 ID 仍应为“platform-001”
9. 非目标
本故事不包含批量重试。
本故事不包含修改任务参数后再重试。
本故事不包含自动定时重试。
本故事不包含重试次数策略配置。
10. 可拆分技术任务
- 定义任务状态流转规则
- 增加重新提交接口
- 校验只有失败任务可以重试
- 生成待处理的重试任务
- 发布或记录重试消息
- 实现重试任务消费者
- 调用三方模型平台重新提交任务
- 保存新的平台任务 ID
- 记录失败原因和历史信息
- 处理重复消费幂等问题
- 前端任务详情页增加“重新提交”按钮
- 前端根据任务状态控制按钮显示
- 补充接口、UseCase、消费者、幂等、异常场景测试
这个例子里,用户故事没有直接写数据库怎么设计,也没有一开始写接口路径。
它先把业务目标、价值、规则、边界说清楚,然后再拆后端技术验收场景和技术任务。
从故事到代码的开发顺序
如果结合用户故事、BDD、DDD 和后端技术验收,更合理的顺序是:
用户目标
↓
业务规则
↓
验收标准
↓
BDD 场景 / 测试用例设计
↓
领域模型
↓
接口设计
↓
数据设计
↓
代码实现
↓
自动化测试执行 / 回归验证
更进一步,可以理解成:
用户故事
↓
业务规则讨论
↓
示例澄清
↓
用户故事验收场景
↓
后端技术验收场景
↓
领域模型设计
↓
端口 / 适配器设计
↓
接口与数据结构设计
↓
实现业务代码
↓
运行自动化验收测试
↓
重构
注意,测试不是最后才想。
更准确地说:
测试场景先定义
代码围绕场景实现
自动化验证最后确认
BDD 的核心是:
先定义行为,再实现行为,最后验证行为。
小团队如何落地
如果你是小厂初级程序员,不一定非要推动团队引入完整的敏捷流程。
你可以先从自己做起。
当产品或领导给你一个需求时,不要马上写代码,而是先在心里把它翻译成用户故事:
这个需求是谁要用?
他想完成什么?
他为什么需要这个能力?
做到什么程度算完成?
哪些情况不包含在这次范围内?
你可以这样和产品沟通:
我确认一下,这个需求是不是可以理解为:
作为一个内容运营人员,
我希望可以查看 AI 任务的处理状态,
以便及时发现失败任务并进行处理。
这次我们先支持任务状态展示和失败原因查看,
暂时不包含批量重试和自动重试,对吗?
如果你是后端,可以继续追问技术验收相关的问题:
哪些任务状态允许重试?
重试后任务状态应该变成什么?
重试是同步提交,还是进入待执行队列?
三方平台失败后状态如何处理?
失败原因是否覆盖,还是保留历史?
重复点击重试按钮如何处理?
消息重复消费是否要求幂等?
状态更新和消息发布是否需要事务一致性?
这些不是“问得太细”,而是后端必须确认的关键规则。
为什么这套方法能降低不确定性
很多初级研发害怕沟通,本质上不是怕说话,而是怕自己问得不专业。
但如果掌握用户故事和验收场景的思维,就会发现你可以围绕业务目标和系统行为提问:
这个功能主要给谁用?
用户使用这个功能是为了解决什么问题?
成功之后用户能获得什么结果?
失败情况系统应该怎么处理?
这次需求有没有不包含的范围?
哪些状态下按钮应该展示?
哪些状态下后端应该拒绝?
哪些外部异常需要被记录?
哪些重复操作需要幂等?
这些问题不是“菜鸟问题”,而是非常专业的需求澄清问题。
真正低效的沟通不是问题太多,而是问题问不到点上。 真正危险的开发不是写得慢,而是不确认需求就开始写。
用户故事的意义,就是帮助研发从“被动接需求”变成“主动理解业务”。
总结:先定义行为,再实现行为
用户故事不是大厂才需要的流程,也不是产品经理写文档时的形式主义。
对小厂初级程序员来说,用户故事是一种非常实用的需求理解工具。
它可以帮助我们:
理解谁是真正的用户
明确功能背后的业务价值
提前发现需求边界
减少开发过程中的返工
让研发、产品、测试对完成标准达成一致
把需求自然转换成测试场景
让后端技术验收更有边界
提升沟通质量和开发效率
最后总结成几句话:
用户故事写业务价值,不写技术实现。
验收标准写可验证行为,不写模糊描述。
BDD 场景先定义行为,再驱动实现。
用户故事验收场景写用户可感知的业务结果。
后端技术验收场景写后端边界收到输入后的可验证行为。
技术任务从用户故事拆出来,不要把技术任务伪装成用户故事。
作为初级程序员,不需要一开始就掌握所有敏捷理论。
但至少要学会一件事:
在写代码之前,先把用户、目标、价值、业务规则和验收标准说清楚。
这一步做好了,后面的开发才会更稳、更快,也更不容易担惊受怕。
开发前的最小检查清单
- 用户角色是否具体,而不是笼统的“用户”?
- 目标是否表达业务行为,而不是技术实现?
- 价值是否说明为什么值得做?
- 正常路径、异常路径和边界条件是否都有例子?
- 验收结果能否从用户或外部系统视角观察?
- 非目标是否写清楚,避免范围在开发中不断扩大?
- 故事是否足够小,可以在一个短周期内完成并获得反馈?
- 接口、数据库、消息和测试工作是否拆成了技术任务?
参考资料
- Agile Alliance:User Stories
- Agile Alliance:INVEST 与 Given-When-Then 术语
- Cucumber:Gherkin Reference
- Google Technical Writing:明确读者、范围和阅读目标
