Config Manager - Evomap Asset
类型安全的 C 语言配置管理库,支持动态键值对与文件加载。
下载 30
提供单元、集成与端到端测试的实用模式与框架指导。
openclaw skills install @wpank/testing-patterns命令、参数、文件名以原文为准
编写能发现缺陷的测试,而不是仅仅通过的测试。 —— 通过覆盖率建立信心,通过隔离实现速度。
| 层级 | 比例 | 速度 | 成本 | 信心 | 范围 |
|---|---|---|---|---|---|
| 单元测试 | ~70% | 毫秒级 | 低 | 低(隔离) | 单个函数/类 |
| 集成测试 | ~20% | 秒级 | 中等 | 中等 | 模块边界、API、数据库 |
| 端到端测试 | ~10% | 分钟级 | 高 | 高(贴近真实场景) | 完整用户流程 |
规则: 如果你的端到端测试数量超过单元测试,请倒置金字塔结构。
| 模式 | 使用时机 | 结构 |
|---|---|---|
| 安排-行动-断言(AAA) | 所有单元测试的默认选择 | 设置、执行、验证 |
| 给定-当-则(GWT) | BDD 风格,行为导向 | 前置条件、操作、结果 |
| 参数化测试 | 相同逻辑需多组输入验证 | 数据驱动测试用例 |
| 快照测试 | UI 组件、序列化输出 | 与保存的基线对比 |
| 基于属性的测试 | 数学不变性验证 | 生成随机输入,断言属性 |
所有单元测试的默认结构。清晰地分离设置、执行和验证部分,使测试更易读且易于维护。
// 清晰的 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);
});根据场景选择合适的测试替身类型。每种都有其特定用途。
| 替身类型 | 目的 | 使用时机 | 示例 |
|---|---|---|---|
| 桩(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');
});// 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(),
);| 框架 | 语言 | 类型 | 测试运行器 | 断言方式 |
|---|---|---|---|---|
| Jest | JS/TS | 单元/集成 | 内置 | expect() |
| Vitest | JS/TS | 单元/集成 | Vite 原生支持 | expect()(兼容 Jest) |
| Playwright | JS/TS/Python | 端到端 | 内置 | expect() / 选择器 |
| Cypress | JS/TS | 端到端 | 内置 | cy.should() |
| pytest | Python | 单元/集成 | 内置 | assert |
| Go testing | Go | 单元/集成 | go test | t.Error() / testify |
| Rust | Rust | 单元/集成 | cargo test | assert!() / assert_eq!() |
| JUnit 5 | Java/Kotlin | 单元/集成 | 内置 | assertEquals() |
| RSpec | Ruby | 单元/集成 | 内置 | expect().to |
| PHPUnit | PHP | 单元/集成 | 内置 | $this->assert*() |
| xUnit | C# | 单元/集成 | 内置 | 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 中重置状态 |
| 在测试中使用 sleep | sleep(2000) 慢且不可靠 | 使用显式等待、轮询或事件机制 |
| 庞大的设置代码 | 50 行设置代码掩盖意图 | 提取工厂函数/构建器/固定数据 |
| 无断言的测试 | 测试运行但未验证任何内容 | 每个测试必须包含断言或期望 |
| 过度模拟 | 模拟一切导致测试无实际意义 | 仅模拟外部边界(如网络、数据库、文件系统、时钟) |
| 复制粘贴的测试 | 重复测试逐渐偏离并失效 | 使用参数化测试或辅助函数 |
| 测试框架本身 | 验证第三方库是否正常工作 | 测试*你的*逻辑,信任依赖项 |
| 忽略测试失败 | skip、xit、@Disabled 累积成负担 | 修复或删除 — 永不长期保留跳过测试 |
| 与数据库强耦合 | 模式变更导致测试失败 | 使用仓库模式 + 模拟数据进行单元测试 |
| 单个巨型测试 | 一个测试覆盖 10 个场景 | 拆分为专注、命名清晰的独立测试 |
| 修复缺陷后无回归测试 | 缺陷后期重现 | 每次修复缺陷都应添加回归测试 |
sleep()** — 使用显式等待、轮询、事件或自动重试的断言| 应该做 | 不应该做 |
|---|---|
| 测试行为而非实现 | 事无巨细地模拟 |
| 在修复缺陷前先写测试 | 为快速发布而跳过测试 |
| 保持测试快速且确定性 | 使用 sleep() 或共享状态 |
| 使用工厂函数生成测试数据 | 复制粘贴测试设置代码 |
| 在系统边界处模拟 | 模拟内部函数 |
| 使用描述性名称命名测试 | 使用 test1、test2 等模糊名称 |
| 每次提交都在 CI 中运行测试 | 仅在本地运行测试 |
| 删除或修复被跳过的测试 | 让 @skip 无限累积 |
| 使用参数化测试处理变体 | 重复编写测试代码 |
| 通过依赖注入提升可测试性 | 硬编码依赖项 |
记住: 测试是一张安全网 — 一个快速、可靠的测试套件让你可以毫无畏惧地重构,并自信地交付。
已收录 8 个 Skill