ATDD:验收测试驱动开发
什么是 ATDD
验收测试驱动开发(Acceptance Test-Driven Development)是 TDD 的延伸,由 Paul Duvall 等人在敏捷实践中推广。它的核心思想是:
在写任何代码之前,先用可执行的验收测试来定义"完成"的标准。
ATDD 与 TDD 的区别在于层次:
| 维度 | TDD | ATDD |
|---|---|---|
| 视角 | 开发者视角 | 业务/用户视角 |
| 测试粒度 | 单元测试 | 端到端/功能测试 |
| 测试语言 | 代码 | 接近自然语言 |
| 参与者 | 开发者 | 开发+产品+测试 |
| 目标 | 代码正确性 | 功能符合需求 |
ATDD 把验收测试当作团队的"共同编程语言"——产品经理、测试工程师和开发者用同一套测试标准沟通需求。
BDD + AI 的黄金组合
行为驱动开发(BDD)是 ATDD 的一种流行实现形式,使用 Gherkin 语法(Given/When/Then)编写可读性极高的测试规格。
这种语法对 AI 来说是极佳的输入格式:
- 结构化:Given/When/Then 强迫你把需求拆解为前置条件、操作和期望结果
- 无歧义:比自然语言更精确,比代码更易读
- 可直接用作 Prompt:Gherkin 场景可以直接粘贴给 AI 作为实现指令
Gherkin 语法速览
gherkin
Feature: 用户登录
Scenario: 正确密码登录成功
Given 用户 "alice@example.com" 已注册,密码为 "secret123"
When 用户用邮箱 "alice@example.com" 和密码 "secret123" 尝试登录
Then 登录应该成功
And 返回有效的 JWT token
Scenario: 错误密码登录失败
Given 用户 "alice@example.com" 已注册
When 用户用邮箱 "alice@example.com" 和密码 "wrongpass" 尝试登录
Then 登录应该失败
And 返回错误信息 "邮箱或密码错误"
And 不返回任何 tokenATDD + AI 工作流
产品/团队 → 定义验收标准(用户故事 + 场景)
↓
你 → 将验收标准转写为 Gherkin 规格
↓
AI → 根据 Gherkin 生成测试代码(步骤定义)
↓
你 → 运行测试,确认失败(需求未实现)
↓
AI → 逐步实现功能,直到所有验收测试通过
↓
你 → Code Review + 确认绿灯
↓
完成!实战示例:电商订单状态机
第一步:定义验收标准
与产品讨论后,整理出以下用户故事:
作为买家,我希望订单状态能正确流转,
以便我了解我的购买进度。
验收标准:
1. 新创建的订单状态为"待支付"
2. 支付成功后状态变为"已支付"
3. 已支付的订单可以发货,状态变为"已发货"
4. 已发货的订单可以确认收货,状态变为"已完成"
5. 待支付状态可以取消,状态变为"已取消"
6. 已支付/已发货状态不能直接取消
7. 已完成/已取消的订单不能进行任何状态变更第二步:转写为 Gherkin
gherkin
Feature: 订单状态流转
Background:
Given 系统中存在一个有效用户
Scenario: 新订单默认为待支付状态
When 用户创建一个新订单
Then 订单状态应为 "PENDING_PAYMENT"
Scenario: 支付成功后订单状态更新
Given 用户有一个状态为 "PENDING_PAYMENT" 的订单
When 支付成功回调触发
Then 订单状态应为 "PAID"
Scenario: 已支付订单可以发货
Given 用户有一个状态为 "PAID" 的订单
When 商家执行发货操作
Then 订单状态应为 "SHIPPED"
Scenario: 已发货订单确认收货
Given 用户有一个状态为 "SHIPPED" 的订单
When 用户确认收货
Then 订单状态应为 "COMPLETED"
Scenario: 待支付订单可以取消
Given 用户有一个状态为 "PENDING_PAYMENT" 的订单
When 用户取消订单
Then 订单状态应为 "CANCELLED"
Scenario Outline: 不可取消的订单状态
Given 用户有一个状态为 "<status>" 的订单
When 用户尝试取消订单
Then 操作应该失败
And 错误信息应包含 "当前订单状态不允许取消"
Examples:
| status |
| PAID |
| SHIPPED |
| COMPLETED |
| CANCELLED |第三步:让 AI 生成测试代码
将以上 Gherkin 发给 AI,要求用 TypeScript + Vitest 实现(不使用 Cucumber,直接翻译为测试用例):
typescript
// order-state.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { Order, OrderStatus } from './order'
describe('订单状态流转', () => {
let order: Order
describe('新订单默认为待支付状态', () => {
it('新创建的订单状态为 PENDING_PAYMENT', () => {
order = new Order({ userId: 'user-1' })
expect(order.status).toBe(OrderStatus.PENDING_PAYMENT)
})
})
describe('支付流转', () => {
beforeEach(() => {
order = new Order({ userId: 'user-1' })
})
it('支付成功后状态变为 PAID', () => {
order.pay()
expect(order.status).toBe(OrderStatus.PAID)
})
it('已支付订单发货后状态变为 SHIPPED', () => {
order.pay()
order.ship()
expect(order.status).toBe(OrderStatus.SHIPPED)
})
it('已发货订单确认收货后状态变为 COMPLETED', () => {
order.pay()
order.ship()
order.complete()
expect(order.status).toBe(OrderStatus.COMPLETED)
})
})
describe('取消逻辑', () => {
it('待支付订单可以取消', () => {
order = new Order({ userId: 'user-1' })
order.cancel()
expect(order.status).toBe(OrderStatus.CANCELLED)
})
it.each([
OrderStatus.PAID,
OrderStatus.SHIPPED,
OrderStatus.COMPLETED,
OrderStatus.CANCELLED,
])('状态为 %s 的订单不能取消', (status) => {
order = Order.fromStatus(status)
expect(() => order.cancel()).toThrow('当前订单状态不允许取消')
})
})
})第四步:AI 生成实现代码
typescript
// order.ts
export enum OrderStatus {
PENDING_PAYMENT = 'PENDING_PAYMENT',
PAID = 'PAID',
SHIPPED = 'SHIPPED',
COMPLETED = 'COMPLETED',
CANCELLED = 'CANCELLED',
}
interface OrderOptions {
userId: string
status?: OrderStatus
}
export class Order {
status: OrderStatus
userId: string
constructor({ userId, status = OrderStatus.PENDING_PAYMENT }: OrderOptions) {
this.userId = userId
this.status = status
}
static fromStatus(status: OrderStatus): Order {
return new Order({ userId: 'test-user', status })
}
pay(): void {
this.assertStatus(OrderStatus.PENDING_PAYMENT, 'pay')
this.status = OrderStatus.PAID
}
ship(): void {
this.assertStatus(OrderStatus.PAID, 'ship')
this.status = OrderStatus.SHIPPED
}
complete(): void {
this.assertStatus(OrderStatus.SHIPPED, 'complete')
this.status = OrderStatus.COMPLETED
}
cancel(): void {
const cancellable = [OrderStatus.PENDING_PAYMENT]
if (!cancellable.includes(this.status)) {
throw new Error('当前订单状态不允许取消')
}
this.status = OrderStatus.CANCELLED
}
private assertStatus(expected: OrderStatus, operation: string): void {
if (this.status !== expected) {
throw new Error(
`操作 ${operation} 需要订单状态为 ${expected},当前状态为 ${this.status}`
)
}
}
}ATDD 在 Vibe Coding 中的价值
Builder.io 的实践表明,在 AI 辅助开发中,验收测试充当了"意图锚点"的角色:
1. 防止 AI 漂移
长对话中,AI 可能逐渐偏离最初的需求。验收测试是客观标准,测试通过了就是通过了,不受对话上下文影响。
2. 分解复杂需求
一个功能可以有多个 Gherkin 场景。每次让 AI 只实现一个场景,迭代推进,比一次性塞给 AI 所有需求更可靠。
3. 文档即测试
Gherkin 既是测试,也是活文档。产品经理可以直接阅读测试文件理解系统行为,减少沟通成本。
小结
ATDD 把需求澄清的工作提前,把"完成"的定义变得精确可测。配合 AI,这个过程变得极其高效:你定义场景,AI 生成测试和实现,测试保证 AI 不偏题。这是 Vibe Coding 中最值得建立的工程习惯之一。