← [ BACK_TO_NOTES ]

Arrange-Act-Assert 测试结构

AAA 是跨语言通用的测试组织模式:先准备上下文,再执行唯一关键行为,最后验证可观察结果,使测试意图清晰且容易定位失败。

#concept #growing #test #testing

[!info] related notes

  • 所属 MOC: testing-moc
  • 前置概念:
  • 并列概念:
  • 易混淆概念:
  • 关系笔记:

Arrange-Act-Assert 测试结构

一句话定义

Arrange-Act-Assert(AAA)是一种组织单个测试案例的通用思维模板:

Arrange  准备测试世界
   ↓
Act      执行这条测试唯一关心的行为
   ↓
Assert   验证外部可观察结果

它不是 Vitest、pytest、JUnit 或 Go testing 的专属语法,而是跨语言、跨框架的测试设计模式。

为什么 AAA 有用

测试最常见的问题不是“不会写断言”,而是一个测试里混杂了太多准备、行为和验证,读者无法快速回答:

  1. 这个场景的前提是什么?
  2. 真正发生了什么动作?
  3. 最后什么结果必须成立?

AAA 把这三个问题显式分开,让测试既是自动验证,也是行为文档。

Arrange:准备测试世界

Arrange 负责建立测试执行前所需的上下文,例如:

  • 构造输入数据
  • 创建对象或组件
  • 准备 fixture
  • 创建测试数据库记录
  • 创建 React Provider / Test Harness
  • Stub 或 Mock 外部依赖
  • 设置 spy
  • 准备用户交互对象

例如 React:

const review = vi.spyOn(workspaceApi, "reviewFact").mockResolvedValue({
  fact: {} as never,
});
const user = userEvent.setup();
render(<BodyStateWorkbench ... />);

例如 Python:

user = User(balance=100)
service = PaymentService(fake_gateway)

Arrange 的目标不是“把所有内部状态都手工设置好”,而是建立足以触发目标行为的最小世界。

Act:执行唯一关键行为

Act 是这条测试真正要验证的动作,通常应尽量只有一个主要行为。

React:

await user.click(screen.getByRole("button", { name: "确认记录" }));

Python:

result = service.pay(user, 30)

HTTP:

response = client.post("/api/orders", json=payload)

如果一条测试有很多彼此独立的 Act,通常意味着:

  • 测试覆盖了多个行为
  • 场景过大
  • 更适合拆成多个测试

但“一个用户行为本身由多个步骤组成”并不一定需要拆。例如登录可能天然包含输入用户名、输入密码、点击提交,这些步骤共同构成一个业务动作。

Assert:验证可观察结果

Assert 负责回答:如果这个行为正确,外部能够观察到什么?

常见断言对象:

  • 返回值
  • DOM 中用户可见的文本和控件
  • 状态变化
  • 数据库持久化结果
  • HTTP status / body
  • 事件是否发布
  • 外部边界是否以正确参数被调用

React:

expect(screen.getByText("疼痛")).toBeInTheDocument();

Mock interaction:

expect(review).toHaveBeenCalledWith("candidate-1", 9, "confirmed");

Assert 应优先验证业务行为,而不是内部实现细节。

AAA 与 Given-When-Then

BDD 常使用 Given-When-Then:

Given  已知某个前提
When   当发生某个行为
Then   那么应该得到某个结果

它与 AAA 基本对应:

AAABDD含义
ArrangeGiven建立上下文
ActWhen执行行为
AssertThen验证结果

区别主要是表达风格:

  • AAA 更常用于代码结构
  • Given-When-Then 更强调业务场景语言

底层思维一致。

AAA 与不同语言

Vitest / Jest

it("confirms a fact", async () => {
  // Arrange
  const review = vi.spyOn(api, "reviewFact").mockResolvedValue({});
  const user = userEvent.setup();
  render(<Component />);

  // Act
  await user.click(screen.getByRole("button", { name: "确认" }));

  // Assert
  expect(review).toHaveBeenCalledOnce();
});

pytest

def test_withdraw():
    # Arrange
    account = Account(balance=100)

    # Act
    account.withdraw(30)

    # Assert
    assert account.balance == 70

Go

func TestWithdraw(t *testing.T) {
    // Arrange
    account := Account{Balance: 100}

    // Act
    account.Withdraw(30)

    // Assert
    if account.Balance != 70 {
        t.Fatalf("got %d", account.Balance)
    }
}

JUnit

@Test
void withdrawsMoney() {
    // Arrange
    var account = new Account(100);

    // Act
    account.withdraw(30);

    // Assert
    assertEquals(70, account.getBalance());
}

框架 API 完全不同,但测试结构没有变化。

一个高价值测试通常还有第四个隐含步骤:Cleanup

严格来说 AAA 只有三段,但真实测试还可能需要清理:

  • 恢复 mock
  • 清空数据库
  • 删除临时文件
  • 关闭 server
  • 清除 QueryClient cache

很多框架通过 afterEach、fixture teardown、defer 等机制承担 Cleanup。

Cleanup 是测试隔离的一部分,不改变 AAA 的主体结构。

Positive Test 与 Negative Test

AAA 描述的是单个测试内部结构;测试集合还要考虑边界两侧。

例如 BodyState:

Positive
symptom fact
→ 应该显示“后来改善 / 后来加重 / 后来恢复”

Negative
lifestyle fact
→ 不应该显示这些 symptom actions

两个测试一起定义真正的业务边界。

只写 Negative Test 时,开发者可能直接删掉所有按钮也让测试通过;Positive Test 会阻止这种错误修复。

与 Regression Test 的关系

AAA 回答“一个测试怎么组织”,Regression Test 回答“为什么写这个测试”。

例如:

历史 bug:lifestyle 也显示 symptom actions
       ↓
写 regression test
       ↓
内部仍然使用 AAA 组织

所以一条测试可以同时是:

  • 层级:Component Test
  • 目的:Regression Test
  • 结构:AAA
  • 手段:Mock API boundary

这些概念不在同一个维度。

与 TDD 的关系

TDD 的:

Red → Green → Refactor

描述开发循环。

AAA 描述每个测试案例内部结构。

它们经常一起使用:

  1. 用 AAA 写出一个清晰测试
  2. 运行得到 Red
  3. 写最小实现变 Green
  4. Refactor 生产代码和测试辅助代码

常见误区

Arrange 太庞大

如果一个测试需要几百行数据准备,优先考虑:

  • Test Data Builder
  • Fixture
  • Factory
  • Test Harness
  • 缩小测试边界

不要因为追求 DRY 把所有业务关键输入都隐藏到 helper 里。

Act 混入断言

await expect(service.run()).resolves.toEqual(...)

语法上没有错,但复杂场景中分开 Act 与 Assert 往往更易读。

Assert 盯内部实现

例如:

expect(component.internalState.mode).toBe("open")

如果真正需求是用户看到弹窗,更稳的断言是:

expect(screen.getByRole("dialog")).toBeInTheDocument()

为形式而强行写注释

简单测试不一定必须显式写 // Arrange、// Act、// Assert。

真正重要的是结构清楚,而不是拥有三个注释。

最短记忆方式

Arrange 建世界,Act 做一件事,Assert 看结果。

再进一步:

测试对外部行为负责,不对内部实现负责。

创建于 2026/8/31 更新于 2026/8/31
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本