testing-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Patterns
测试模式
Test Hierarchy (ALWAYS prefer simpler)
测试层级(优先选择更简单的类型)
- Unit tests (preferred) - Pure functions, parsers, Effect services
- TRPC Integration (ask first) - Full TRPC stack with PGlite
- E2E (ask + justify) - Browser automation, slowest
- 单元测试(首选)- 纯函数、解析器、Effect服务
- TRPC集成测试(需先询问)- 搭配PGlite的完整TRPC栈
- E2E测试(需询问并说明理由)- 浏览器自动化,速度最慢
When to Use Each
各测试类型的适用场景
| Situation | Test Type | Action |
|---|---|---|
| Pure function, parser, util | Unit | Write immediately |
| Effect service with dependencies | Unit with mock layers | Write immediately |
| TRPC procedure (DB logic) | TRPC Integration | Ask user first |
| User-facing flow, UI behavior | E2E | Ask + warn about maintenance |
| 场景 | 测试类型 | 操作说明 |
|---|---|---|
| 纯函数、解析器、工具函数 | 单元测试 | 立即编写 |
| 带有依赖的Effect服务 | 带模拟层的单元测试 | 立即编写 |
| TRPC过程(包含数据库逻辑) | TRPC集成测试 | 先询问用户 |
| 用户交互流程、UI行为 | E2E测试 | 询问用户并提醒维护成本 |
Test File Locations
测试文件存放位置
| Code Location | Test Location |
|---|---|
| |
| |
| |
| 代码位置 | 测试文件位置 |
|---|---|
| |
| |
| |
Unit Test Patterns
单元测试模式
Basic Vitest
基础Vitest测试
typescript
import { describe, expect, it } from "vitest";
describe("parseResourceSize", () => {
it("parses Ki units", () => {
expect(parseResourceSize("512Ki")).toBe(524288);
});
});typescript
import { describe, expect, it } from "vitest";
describe("parseResourceSize", () => {
it("parses Ki units", () => {
expect(parseResourceSize("512Ki")).toBe(524288);
});
});Effect with @effect/vitest
搭配@effect/vitest的Effect测试
typescript
import { describe, expect, it } from "@effect/vitest";
import { Effect, Either, Layer } from "effect";
describe("K8sMetricsService", () => {
// Mock layer factory
const createMockLayer = (responses: Map<string, unknown>) =>
Layer.succeed(K8sHttpClient, {
request: (params) => Effect.succeed(responses.get(params.path)),
});
const testLayer = K8sMetricsService.layer.pipe(
Layer.provide(createMockLayer(mockResponses))
);
it.effect("collects metrics", () =>
Effect.gen(function* () {
const service = yield* K8sMetricsService;
const result = yield* service.collectMetrics({ ... });
expect(result.namespaces).toHaveLength(3);
}).pipe(Effect.provide(testLayer))
);
// Error handling with Either.match
it.effect("handles error case", () =>
Effect.gen(function* () {
const result = yield* myEffect.pipe(Effect.either);
Either.match(result, {
onLeft: (error) => {
expect(error._tag).toBe("K8sConnectionError");
},
onRight: () => {
expect.fail("Expected Left but got Right");
},
});
}).pipe(Effect.provide(testLayer))
);
});typescript
import { describe, expect, it } from "@effect/vitest";
import { Effect, Either, Layer } from "effect";
describe("K8sMetricsService", () => {
// 模拟层工厂函数
const createMockLayer = (responses: Map<string, unknown>) =>
Layer.succeed(K8sHttpClient, {
request: (params) => Effect.succeed(responses.get(params.path)),
});
const testLayer = K8sMetricsService.layer.pipe(
Layer.provide(createMockLayer(mockResponses))
);
it.effect("collects metrics", () =>
Effect.gen(function* () {
const service = yield* K8sMetricsService;
const result = yield* service.collectMetrics({ ... });
expect(result.namespaces).toHaveLength(3);
}).pipe(Effect.provide(testLayer))
);
// 使用Either.match处理错误
it.effect("handles error case", () =>
Effect.gen(function* () {
const result = yield* myEffect.pipe(Effect.either);
Either.match(result, {
onLeft: (error) => {
expect(error._tag).toBe("K8sConnectionError");
},
onRight: () => {
expect.fail("Expected Left but got Right");
},
});
}).pipe(Effect.provide(testLayer))
);
});Live Effect tests (real dependencies)
真实依赖的Effect测试
typescript
it.live("returns success when endpoint is ready", () => {
globalThis.fetch = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
return Effect.gen(function* () {
const svc = yield* HealthCheckService;
const result = yield* svc.checkApiHealth("http://api", {
maxRetries: 1,
});
expect(result.success).toBe(true);
}).pipe(Effect.provide(HealthCheckServiceLive));
});typescript
it.live("returns success when endpoint is ready", () => {
globalThis.fetch = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
return Effect.gen(function* () {
const svc = yield* HealthCheckService;
const result = yield* svc.checkApiHealth("http://api", {
maxRetries: 1,
});
expect(result.success).toBe(true);
}).pipe(Effect.provide(HealthCheckServiceLive));
});TRPC Integration Test Patterns
TRPC集成测试模式
Ask user before writing: "Does an integration test make sense for this TRPC endpoint?"
编写前需询问用户: "这个TRPC接口需要集成测试吗?"
Setup
测试设置
typescript
import { describe, expect, it, beforeEach, afterEach } from "vitest";
import type { PGlite } from "@electric-sql/pglite";
import {
createTestDb,
cleanupTestDb,
type TestDb,
seedUser,
seedOrganization,
seedMember,
seedProject,
} from "@project/db/testing";
import { createTestCaller } from "./trpc-test-utils";
describe("agents.listRuns", () => {
let db: TestDb;
let client: PGlite | undefined;
beforeEach(async () => {
const testDb = await createTestDb();
db = testDb.db;
client = testDb.client;
});
afterEach(async () => {
await cleanupTestDb(client);
client = undefined;
});
it("returns correct results", async () => {
// Seed data
const user = await seedUser(db);
const org = await seedOrganization(db);
await seedMember(db, {
userId: user.id,
organizationId: org.id,
});
const project = await seedProject(db, {
organizationId: org.id,
});
// Create caller with auth context
const caller = createTestCaller({
db,
userId: user.id,
});
// Call TRPC procedure
const result = await caller.agents.listRuns({
projectId: project.id,
page: 1,
pageSize: 10,
});
expect(result.runs).toHaveLength(0);
expect(result.total).toBe(0);
});
});typescript
import { describe, expect, it, beforeEach, afterEach } from "vitest";
import type { PGlite } from "@electric-sql/pglite";
import {
createTestDb,
cleanupTestDb,
type TestDb,
seedUser,
seedOrganization,
seedMember,
seedProject,
} from "@project/db/testing";
import { createTestCaller } from "./trpc-test-utils";
describe("agents.listRuns", () => {
let db: TestDb;
let client: PGlite | undefined;
beforeEach(async () => {
const testDb = await createTestDb();
db = testDb.db;
client = testDb.client;
});
afterEach(async () => {
await cleanupTestDb(client);
client = undefined;
});
it("returns correct results", async () => {
// 填充测试数据
const user = await seedUser(db);
const org = await seedOrganization(db);
await seedMember(db, {
userId: user.id,
organizationId: org.id,
});
const project = await seedProject(db, {
organizationId: org.id,
});
// 创建带认证上下文的调用器
const caller = createTestCaller({
db,
userId: user.id,
});
// 调用TRPC过程
const result = await caller.agents.listRuns({
projectId: project.id,
page: 1,
pageSize: 10,
});
expect(result.runs).toHaveLength(0);
expect(result.total).toBe(0);
});
});Available seed helpers
可用的数据填充工具
typescript
import {
seedUser,
seedOrganization,
seedMember,
seedProject,
seedAgentTemplate,
seedAgentInstance,
seedAgentRun,
seedGitHubIssue,
seedCompleteScenario, // Creates full user -> org -> project -> agent -> run chain
} from "@project/db/testing";typescript
import {
seedUser,
seedOrganization,
seedMember,
seedProject,
seedAgentTemplate,
seedAgentInstance,
seedAgentRun,
seedGitHubIssue,
seedCompleteScenario, // 创建完整的用户 -> 组织 -> 项目 -> Agent -> 运行链
} from "@project/db/testing";E2E Test Patterns
E2E测试模式
Ask user + warn: "E2E tests are the most expensive to maintain. Is this really needed for this feature?"
编写前需询问并提醒用户: "E2E测试的维护成本最高。这个功能真的需要E2E测试吗?"
Basic E2E
基础E2E测试
typescript
import { expect, test } from "@playwright/test";
import { e2eEnv } from "./env";
import { ensureTestUserExists, signInWithEmail } from "./auth-helpers";
const testEmail = e2eEnv.E2E_TEST_EMAIL;
const testPassword = e2eEnv.E2E_TEST_PASSWORD;
test("auth: can sign in with email", async ({ page }) => {
await ensureTestUserExists(page.request, {
email: testEmail,
password: testPassword,
name: "E2E Test User",
});
await signInWithEmail(page, {
email: testEmail,
password: testPassword,
});
await expect(
page.getByRole("heading", {
name: "Dashboard",
exact: true,
}),
).toBeVisible({ timeout: 5_000 });
});typescript
import { expect, test } from "@playwright/test";
import { e2eEnv } from "./env";
import { ensureTestUserExists, signInWithEmail } from "./auth-helpers";
const testEmail = e2eEnv.E2E_TEST_EMAIL;
const testPassword = e2eEnv.E2E_TEST_PASSWORD;
test("auth: can sign in with email", async ({ page }) => {
await ensureTestUserExists(page.request, {
email: testEmail,
password: testPassword,
name: "E2E Test User",
});
await signInWithEmail(page, {
email: testEmail,
password: testPassword,
});
await expect(
page.getByRole("heading", {
name: "Dashboard",
exact: true,
}),
).toBeVisible({ timeout: 5_000 });
});Auth helpers
认证工具函数
typescript
import { signInWithEmail, ensureTestUserExists } from "./auth-helpers";
import { waitForHydration } from "./wait-for-hydration";
// Before interacting with forms
await waitForHydration(page);typescript
import { signInWithEmail, ensureTestUserExists } from "./auth-helpers";
import { waitForHydration } from "./wait-for-hydration";
// 与表单交互前等待 hydration 完成
await waitForHydration(page);Test credentials
测试凭证
typescript
// From e2eEnv
const testEmail = e2eEnv.E2E_TEST_EMAIL; // test@example.com
const testPassword = e2eEnv.E2E_TEST_PASSWORD; // TestPass123typescript
// 来自e2eEnv
const testEmail = e2eEnv.E2E_TEST_EMAIL; // test@example.com
const testPassword = e2eEnv.E2E_TEST_PASSWORD; // TestPass123Commands
测试命令
bash
bun run test # Run all unit + TRPC integration tests
bun run test:watch # Watch mode
bun run test:coverage # With coverage
bun run test:e2e # Run E2E tests
bun run test:e2e:ui # E2E with UIbash
bun run test # 运行所有单元测试 + TRPC集成测试
bun run test:watch # 监听模式
bun run test:coverage # 生成测试覆盖率报告
bun run test:e2e # 运行E2E测试
bun run test:e2e:ui # 带UI界面的E2E测试Run specific test file (FROM PROJECT ROOT, full path required)
运行指定测试文件(需在项目根目录执行,必须使用完整路径)
bun run vitest run packages/common/src/tests/pagination.test.ts
bun run vitest run apps/web-app/src/tests/formatters.test.ts
**WRONG syntax (DO NOT USE):**
```bashbun run vitest run packages/common/src/tests/pagination.test.ts
bun run vitest run apps/web-app/src/tests/formatters.test.ts
**错误语法(禁止使用):**
```bashThese DO NOT work:
以下命令无法正常工作:
bun run test packages/common/src/tests/file.test.ts # script doesn't accept path
cd packages/common && bun run vitest run src/tests/file.test.ts # wrong cwd
---bun run test packages/common/src/tests/file.test.ts # 脚本不接受路径参数
cd packages/common && bun run vitest run src/tests/file.test.ts # 工作目录错误
---Decision Process
测试决策流程
Before writing ANY test:
- Can this be unit tested? -> Write unit test immediately
- Need DB behavior (joins, constraints)? -> Ask: "Does an integration test make sense here?"
- Need browser/UI? -> Ask + warn: "E2E tests are expensive to maintain. Is this necessary?"
Never write integration or E2E tests without user confirmation.
在编写任何测试前,请遵循以下步骤:
- 是否可以用单元测试覆盖? -> 立即编写单元测试
- 需要验证数据库行为(关联查询、约束)? -> 询问用户:"这里需要集成测试吗?"
- 需要验证浏览器/UI行为? -> 询问并提醒用户:"E2E测试维护成本很高,这真的有必要吗?"
未经用户确认,请勿编写集成测试或E2E测试。