名称: 测试模式 描述: Jest 和 Playwright 的测试模式。在编写测试、设置测试夹具或验证 RLS 执行时使用。路由到现有测试约定并提供证据模板。
测试模式技能
目的
指导一致和有效的测试。路由到现有测试模式并为 Linear 提供证据模板。
何时应用此技能
在以下情况下调用此技能:
- 编写新的单元测试
- 创建集成测试
- 设置带 RLS 的测试夹具
- 运行测试套件
- 为 Linear 打包测试证据
关键规则
❌ 禁止的模式
// 禁止:在测试中直接调用 Prisma(绕过 RLS)
const user = await prisma.user.findUnique({ where: { user_id } });
// 禁止:共享测试状态(导致不稳定测试)
let sharedUser: User;
beforeAll(() => { sharedUser = createUser(); });
// 禁止:硬编码 ID(测试污染)
const userId = "user-123";
// 禁止:缺少清理(泄漏测试)
it("创建用户", async () => {
await prisma.user.create({ data: userData });
// 没有清理!
});
✅ 正确的模式
// 正确:使用 RLS 上下文助手
const user = await withSystemContext(prisma, "测试", async (client) => {
return client.user.findUnique({ where: { user_id } });
});
// 正确:每个测试的隔离测试状态
beforeEach(() => {
const testUser = createTestUser();
});
// 正确:唯一标识符
const userId = `user-${crypto.randomUUID()}`;
const email = `test-${Date.now()}@example.com`;
// 正确:适当的清理
afterEach(async () => {
await withSystemContext(prisma, "测试", async (client) => {
await client.user.deleteMany({ where: { email: { contains: "test-" } } });
});
});
测试目录结构
__tests__/
├── unit/ # 快速、隔离的测试
│ ├── components/ # React 组件测试
│ ├── lib/ # 库函数测试
│ ├── services/ # 服务层测试
│ └── user/ # 用户助手测试
├── integration/ # API 和数据库测试
├── database/ # 数据库助手测试
├── e2e/ # 端到端测试 (Playwright)
├── payments/ # 支付流程测试
└── setup.ts # 全局测试设置
配置文件
- Jest 配置:
jest.config.js - 测试设置:
__tests__/setup.ts - Playwright 配置:
playwright.config.ts
RLS 感知测试
设置测试上下文
在测试中始终使用 RLS 上下文助手:
import { withUserContext, withSystemContext } from "@/lib/rls-context";
import { prisma } from "@/lib/prisma";
describe("用户支付", () => {
const testUserId = "测试用户-123";
beforeEach(async () => {
// 使用 RLS 上下文创建测试用户
await withSystemContext(prisma, "测试", async (client) => {
await client.user.create({
data: {
user_id: testUserId,
email: `test-${Date.now()}@example.com`,
first_name: "测试",
last_name: "用户",
},
});
});
});
it("应仅看到自己的支付", async () => {
const payments = await withUserContext(
prisma,
testUserId,
async (client) => {
return client.payments.findMany();
},
);
// RLS 确保仅返回此用户的支付
expect(payments.every((p) => p.user_id === testUserId)).toBe(true);
});
});
测试隔离
使用唯一标识符防止测试污染:
const uniqueEmail = `test-${Date.now()}@example.com`;
const uniqueUserId = `user-${crypto.randomUUID()}`;
测试命令
# 运行所有单元测试
yarn test:unit
# 运行集成测试
yarn test:integration
# 运行特定测试文件
yarn jest __tests__/unit/components/my-component.test.tsx
# 运行匹配模式的测试
yarn jest --testNamePattern="应处理"
# 运行带覆盖率的测试
yarn test:unit --coverage
# 运行 E2E 测试
yarn test:e2e
常见模式
组件测试
import { render, screen, fireEvent } from "@testing-library/react";
import { MyComponent } from "@/components/my-component";
describe("MyComponent", () => {
it("正确渲染", () => {
render(<MyComponent />);
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("处理点击事件", async () => {
const onClickMock = jest.fn();
render(<MyComponent onClick={onClickMock} />);
fireEvent.click(screen.getByRole("button"));
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
API 路由测试
import { GET } from "@/app/api/my-route/route";
import { NextRequest } from "next/server";
describe("GET /api/my-route", () => {
it("返回 200 带数据", async () => {
const request = new NextRequest("http://localhost:3000/api/my-route");
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveProperty("success", true);
});
});
模拟 Prisma
jest.mock("@/lib/prisma", () => ({
prisma: {
user: {
findUnique: jest.fn(),
create: jest.fn(),
},
},
}));
Linear 的证据模板
完成测试工作时,附加此证据块:
**测试执行证据**
**测试套件**: [单元/集成/E2E]
**更改的文件**: [列出文件]
**测试结果:**
- 总测试: [X]
- 通过: [X]
- 失败: [0]
- 跳过: [X]
**覆盖率** (如适用):
- 语句: X%
- 分支: X%
- 函数: X%
- 行: X%
**运行的命令:**
\`\`\`bash
yarn test:unit --coverage
\`\`\`
**输出:**
[粘贴相关测试输出]
推送前验证
推送前始终运行:
yarn ci:validate
这运行:
- 类型检查
- ESLint
- 单元测试
- 格式检查
权威参考
- Jest 配置:
jest.config.js - 测试设置:
__tests__/setup.ts - RLS 上下文:
lib/rls-context.ts - CI 验证:
package.json脚本