Testing Patterns

提供单元、集成与端到端测试的实用模式与框架指导。

已扫描
适合谁
前端/后端开发工程师、团队技术负责人与测试架构师
不适合谁
无编程基础的非技术人员、仅需使用现成功能而无需关注测试逻辑的用户
国内可用性
未知。数据库暂未记录国内可用性结论。
安装难度
新手友好(★☆☆)。基于终端操作、依赖、API Key 和本地环境要求的初步判断。

安装与下载

openclaw skills install @wpank/testing-patterns

Skill 说明

命令、参数、文件名以原文为准

测试模式

编写能发现缺陷的测试,而不是仅仅通过的测试。 —— 通过覆盖率建立信心,通过隔离实现速度。


测试金字塔

层级比例速度成本信心范围
单元测试~70%毫秒级低(隔离)单个函数/类
集成测试~20%秒级中等中等模块边界、API、数据库
端到端测试~10%分钟级高(贴近真实场景)完整用户流程

规则: 如果你的端到端测试数量超过单元测试,请倒置金字塔结构。


单元测试模式

核心模式

模式使用时机结构
安排-行动-断言(AAA)所有单元测试的默认选择设置、执行、验证
给定-当-则(GWT)BDD 风格,行为导向前置条件、操作、结果
参数化测试相同逻辑需多组输入验证数据驱动测试用例
快照测试UI 组件、序列化输出与保存的基线对比
基于属性的测试数学不变性验证生成随机输入,断言属性

安排-行动-断言(AAA)

所有单元测试的默认结构。清晰地分离设置、执行和验证部分,使测试更易读且易于维护。

// 清晰的 AAA 结构
test('计算含税订单总价', () => {
  // 安排
  const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }];
  const taxRate = 0.08;

  // 行动
  const total = calculateTotal(items, taxRate);

  // 断言
  expect(total).toBe(27.0);
});

测试替身(Test Doubles)

根据场景选择合适的测试替身类型。每种都有其特定用途。

替身类型目的使用时机示例
桩(Stub)返回预设数据控制间接输入jest.fn().mockReturnValue(42)
模拟(Mock)验证交互行为断言某方法被调用expect(mock).toHaveBeenCalledWith('arg')
间谍(Spy)包装真实实现观察行为而不替换jest.spyOn(service, 'save')
假实现(Fake)简化的可用实现需要接近真实行为内存数据库、假 HTTP 服务器
// 桩 —— 控制间接输入
const getUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' });

// 间谍 —— 观察而不替换
const spy = jest.spyOn(logger, 'warn');
processInvalidInput(data);
expect(spy).toHaveBeenCalledWith('Invalid input received');

// 假实现 —— 轻量级替代
class FakeUserRepo implements UserRepository {
  private users = new Map<string, User>();
  async save(user: User) { this.users.set(user.id, user); }
  async findById(id: string) { return this.users.get(id) ?? null; }
}

参数化测试

当相同逻辑需要在多种输入下验证时使用参数化测试。可避免重复代码,同时提供全面覆盖。

// Vitest/Jest
test.each([
  ['hello', 'HELLO'],
  ['world', 'WORLD'],
  ['', ''],
  ['123abc', '123ABC'],
])('toUpperCase(%s) 返回 %s', (input, expected) => {
  expect(input.toUpperCase()).toBe(expected);
});
# pytest
@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("", ""),
])
def test_to_upper(input, expected):
    assert input.upper() == expected
// Go —— 表驱动测试(惯用写法)
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"zero", 0, 0, 0},
        {"negative", -1, -2, -3},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            if got := Add(tc.a, tc.b); got != tc.expected {
                t.Errorf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.expected)
            }
        })
    }
}

集成测试模式

数据库测试策略

策略方法权衡
事务回滚每个测试包裹在事务中,测试后回滚快速,但可能掩盖提交错误
固定数据/种子测试套件前加载已知数据可预测,但若模式变更则脆弱
工厂函数程序化生成数据灵活,但需更多设置代码
Testcontainers在 Docker 中启动真实数据库接近真实环境,但启动较慢
// 事务回滚模式(Prisma)
beforeEach(async () => {
  await prisma.$executeRaw`BEGIN`;
});
afterEach(async () => {
  await prisma.$executeRaw`ROLLBACK`;
});

test('在数据库中创建用户', async () => {
  const user = await createUser({ name: 'Alice', email: 'a@b.com' });
  const found = await prisma.user.findUnique({ where: { id: user.id } });
  expect(found?.name).toBe('Alice');
});

API 测试

// Supertest(Node.js)
import request from 'supertest';
import { app } from '../src/app';

describe('POST /api/users', () => {
  it('创建用户并返回 201', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'alice@test.com' })
      .expect(201);

    expect(res.body).toMatchObject({
      id: expect.any(String),
      name: 'Alice',
    });
  });

  it('对无效邮箱返回 400', async () => {
    await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: 'not-an-email' })
      .expect(400);
  });
});

模拟的最佳实践

模拟边界,而非实现

基本原则:应在系统边界(外部 API、数据库、文件系统)处进行模拟,绝不应模拟内部领域逻辑。

// 错误示例 —— 模拟内部实现
jest.mock('./utils/formatDate');  // 重构时容易失效

// 正确示例 —— 模拟外部边界
jest.mock('./services/paymentGateway');  // 第三方 API 是边界

何时模拟,何时不模拟

Mock不应 Mock
HTTP API、外部服务纯函数
数据库(在单元测试中)自身领域逻辑
文件系统、网络数据转换
时间/日期(Date.now简单计算
环境变量内部类方法

依赖注入以提升可测试性

将代码结构设计为可在测试中替换依赖项。这是提升代码可测试性的最关键模式。

// 可注入的依赖 —— 易于测试
class OrderService {
  constructor(
    private paymentGateway: PaymentGateway,
    private inventory: InventoryService,
    private notifier: NotificationService,
  ) {}

  async placeOrder(order: Order): Promise<OrderResult> {
    const stock = await this.inventory.check(order.items);
    if (!stock.available) return { status: 'out_of_stock' };

    const payment = await this.paymentGateway.charge(order.total);
    if (!payment.success) return { status: 'payment_failed' };

    await this.notifier.send(order.userId, 'Order confirmed');
    return { status: 'confirmed', id: payment.transactionId };
  }
}

// 在测试中 —— 注入假实现
const service = new OrderService(
  new FakePaymentGateway(),
  new FakeInventory({ available: true }),
  new FakeNotifier(),
);

框架速查表

框架语言类型测试运行器断言方式
JestJS/TS单元/集成内置expect()
VitestJS/TS单元/集成Vite 原生支持expect()(兼容 Jest)
PlaywrightJS/TS/Python端到端内置expect() / 选择器
CypressJS/TS端到端内置cy.should()
pytestPython单元/集成内置assert
Go testingGo单元/集成go testt.Error() / testify
RustRust单元/集成cargo testassert!() / assert_eq!()
JUnit 5Java/Kotlin单元/集成内置assertEquals()
RSpecRuby单元/集成内置expect().to
PHPUnitPHP单元/集成内置$this->assert*()
xUnitC#单元/集成内置Assert.Equal()

测试质量检查清单

质量规则原因
确定性相同输入始终产生相同结果不稳定测试会削弱信任
隔离性测试之间无共享可变状态依赖顺序的测试在 CI 中会失败
快速单元测试:<10ms,集成测试:<1s,端到端测试:<30s慢的测试不会被执行
可读性测试名称应描述场景与预期结果测试即文档
可维护性改动一个行为,只修改一个测试脆弱的测试会拖慢开发
聚焦性每个测试只包含一个逻辑断言失败能准确定位问题

命名规范: test_[单元]_[场景]_[预期结果]should [执行某操作] when [条件 Y]


覆盖率策略

应达到何种覆盖率

目标适用场景理由
80%+ 行覆盖率业务逻辑、工具函数、核心领域代码回报高 —— 可捕获大多数回归问题
90%+ 分支覆盖率支付处理、认证、安全关键路径边界情况在此类代码中至关重要
100% 覆盖率几乎从不追求 —— 投入产出比递减Getter/setter 测试增加噪音,而非信心
变异测试高覆盖率后的重要路径验证测试是否真正能发现缺陷

不应测试的内容

忽略项原因
生成代码(如 Prisma 客户端、protobuf)由工具维护
第三方库内部实现不属于你的责任范围
简单的 getter/setter无逻辑可验证
配置文件应测试其配置的行为表现
console.log / print 语句无业务价值的副作用

测试组织结构

src/
├── services/
│   ├── order.service.ts
│   └── order.service.test.ts      # 与源码共存的单元测试
├── api/
│   └── routes/
│       └── orders.ts
tests/
├── integration/
│   ├── api/
│   │   └── orders.test.ts         # API 集成测试
│   └── db/
│       └── order.repo.test.ts     # 数据库集成测试
├── e2e/
│   ├── pages/                     # 页面对象
│   │   └── checkout.page.ts
│   └── specs/
│       └── checkout.spec.ts       # 端到端测试用例
└── helpers/
    ├── factories.ts               # 测试数据工厂
    └── setup.ts                   # 全局测试设置

规则: 单元测试与源码共存。集成测试和端到端测试应放在独立目录中。


反模式

markdown

测试模式

常见反模式

反模式问题解决方案
测试实现细节重构时测试失败,而非发现缺陷测试行为和输出,而非内部实现
不稳定的测试非确定性失败削弱 CI 信任移除时间、顺序、网络依赖
测试污染共享的可变状态在测试间泄漏beforeEach / setUp 中重置状态
在测试中使用 sleepsleep(2000) 慢且不可靠使用显式等待、轮询或事件机制
庞大的设置代码50 行设置代码掩盖意图提取工厂函数/构建器/固定数据
无断言的测试测试运行但未验证任何内容每个测试必须包含断言或期望
过度模拟模拟一切导致测试无实际意义仅模拟外部边界(如网络、数据库、文件系统、时钟)
复制粘贴的测试重复测试逐渐偏离并失效使用参数化测试或辅助函数
测试框架本身验证第三方库是否正常工作测试*你的*逻辑,信任依赖项
忽略测试失败skipxit@Disabled 累积成负担修复或删除 — 永不长期保留跳过测试
与数据库强耦合模式变更导致测试失败使用仓库模式 + 模拟数据进行单元测试
单个巨型测试一个测试覆盖 10 个场景拆分为专注、命名清晰的独立测试
修复缺陷后无回归测试缺陷后期重现每次修复缺陷都应添加回归测试

绝对不要做

  1. 绝不要测试实现细节而非行为 — 测试应验证代码做什么,而非如何做
  2. **绝不要在测试中使用 sleep()** — 使用显式等待、轮询、事件或自动重试的断言
  3. 绝不要在测试间共享可变状态 — 每个测试应独立设置和清理自身状态
  4. 绝不要编写无断言的测试 — 不断言的测试无法证明任何事情
  5. 绝不要模拟内部领域逻辑 — 仅在系统边界(网络、数据库、文件系统、时钟)处模拟
  6. 绝不要跳过测试而不关联问题并制定恢复计划 — 跳过的测试会逐渐变成永久漏洞
  7. 绝不要让测试套件处于持续失败状态 — 修复或移除并说明理由后再继续
  8. 绝不要以 100% 覆盖率为目标 — 覆盖率是工具,不是目标;关键路径上的强断言优于全局弱断言

总结

应该做不应该做
测试行为而非实现事无巨细地模拟
在修复缺陷前先写测试为快速发布而跳过测试
保持测试快速且确定性使用 sleep() 或共享状态
使用工厂函数生成测试数据复制粘贴测试设置代码
在系统边界处模拟模拟内部函数
使用描述性名称命名测试使用 test1test2 等模糊名称
每次提交都在 CI 中运行测试仅在本地运行测试
删除或修复被跳过的测试@skip 无限累积
使用参数化测试处理变体重复编写测试代码
通过依赖注入提升可测试性硬编码依赖项

记住: 测试是一张安全网 — 一个快速、可靠的测试套件让你可以毫无畏惧地重构,并自信地交付。

W
@wpank

已收录 8 个 Skill

相关推荐