name: testing-strategies description: 测试策略,包括合同测试、快照测试、变异测试、基于属性的测试和测试组织
测试策略
测试结构(安排-行动-断言)
describe("OrderService", () => {
describe("createOrder", () => {
it("用有效项目创建订单并返回订单ID", async () => {
const repo = new InMemoryOrderRepository();
const service = new OrderService(repo);
const input = { customerId: "c1", items: [{ productId: "p1", quantity: 2 }] };
const result = await service.createOrder(input);
expect(result.id).toBeDefined();
expect(result.status).toBe("pending");
expect(result.items).toHaveLength(1);
const saved = await repo.findById(result.id);
expect(saved).toEqual(result);
});
it("拒绝空项目的订单", async () => {
const service = new OrderService(new InMemoryOrderRepository());
await expect(
service.createOrder({ customerId: "c1", items: [] })
).rejects.toThrow("订单必须至少有一个项目");
});
});
});
按行为命名测试,而不是方法名。每个测试应该是独立和自包含的。
合同测试(Pact)
import { PactV4 } from "@pact-foundation/pact";
const provider = new PactV4({
consumer: "OrderService",
provider: "UserService",
});
describe("UserService合同", () => {
it("按ID返回用户", async () => {
await provider
.addInteraction()
.given("ID为user-1的用户存在")
.uponReceiving("对用户user-1的请求")
.withRequest("GET", "/api/users/user-1")
.willRespondWith(200, (builder) => {
builder.jsonBody({
id: "user-1",
name: "Alice",
email: "alice@example.com",
});
})
.executeTest(async (mockServer) => {
const client = new UserClient(mockServer.url);
const user = await client.getUser("user-1");
expect(user.name).toBe("Alice");
});
});
});
合同测试验证消费者期望与提供者能力匹配,无需两个服务同时运行。
快照测试
import { render } from "@testing-library/react";
it("渲染用户资料卡", () => {
const { container } = render(
<UserCard user={{ name: "Alice", email: "alice@example.com", role: "admin" }} />
);
expect(container).toMatchSnapshot();
});
it("使用内联快照渲染订单摘要", () => {
const summary = formatOrderSummary(mockOrder);
expect(summary).toMatchInlineSnapshot(`
"订单 #123
项目数: 3
总计: $45.99
状态: 待处理"
`);
});
对于小输出使用内联快照。在代码审查期间仔细审查快照差异。
基于属性的测试
import fc from "fast-check";
describe("sortUsers", () => {
it("总是返回相同数量的元素", () => {
fc.assert(
fc.property(
fc.array(fc.record({ name: fc.string(), age: fc.nat(120) })),
(users) => {
const sorted = sortUsers(users, "name");
return sorted.length === users.length;
}
)
);
});
it("为任何输入产生排序结果", () => {
fc.assert(
fc.property(
fc.array(fc.record({ name: fc.string(), age: fc.nat(120) })),
(users) => {
const sorted = sortUsers(users, "age");
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].age < sorted[i - 1].age) return false;
}
return true;
}
)
);
});
});
使用测试容器的集成测试
import { PostgreSqlContainer } from "@testcontainers/postgresql";
let container: any;
let db: Database;
beforeAll(async () => {
container = await new PostgreSqlContainer("postgres:16").start();
db = await createDatabase(container.getConnectionUri());
await db.migrate();
}, 60000);
afterAll(async () => {
await db.close();
await container.stop();
});
it("创建并检索用户", async () => {
const user = await db.user.create({ name: "Alice", email: "alice@test.com" });
const found = await db.user.findById(user.id);
expect(found).toEqual(user);
});
测试替身
function createMockEmailService(): EmailService {
const sent: Array<{ to: string; subject: string }> = [];
return {
send: async (to, subject, body) => { sent.push({ to, subject }); },
getSent: () => sent,
};
}
const emailService = createMockEmailService();
const service = new NotificationService(emailService);
await service.notifyUser("user-1", "欢迎");
expect(emailService.getSent()).toHaveLength(1);
expect(emailService.getSent()[0].subject).toBe("欢迎");
反模式
- 测试实现细节而不是行为
- 在测试之间共享可变状态(没有
beforeEach重置) - 编写依赖于执行顺序的测试
- 模拟所有内容而不是在集成测试中使用真实依赖
- 忽略不稳定测试而不是修复根本原因
- 测试琐碎的 getter/setter 而错过边缘情况
检查清单
- [ ] 测试按行为组织,而不是按方法或文件
- [ ] 每个测试遵循安排-行动-断言结构
- [ ] 合同测试验证服务间 API 兼容性
- [ ] 快照测试在代码审查期间审查(不盲目更新)
- [ ] 基于属性的测试覆盖算法代码的 invariants
- [ ] 集成测试使用测试容器用于真实依赖
- [ ] 测试替身是最小且行为聚焦的
- [ ] CI 在不稳定测试检测时失败