testing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing Patterns

测试模式

Test Hierarchy (ALWAYS prefer simpler)

测试层级(优先选择更简单的类型)

  1. Unit tests (preferred) - Pure functions, parsers, Effect services
  2. TRPC Integration (ask first) - Full TRPC stack with PGlite
  3. E2E (ask + justify) - Browser automation, slowest
  1. 单元测试(首选)- 纯函数、解析器、Effect服务
  2. TRPC集成测试(需先询问)- 搭配PGlite的完整TRPC栈
  3. E2E测试(需询问并说明理由)- 浏览器自动化,速度最慢

When to Use Each

各测试类型的适用场景

SituationTest TypeAction
Pure function, parser, utilUnitWrite immediately
Effect service with dependenciesUnit with mock layersWrite immediately
TRPC procedure (DB logic)TRPC IntegrationAsk user first
User-facing flow, UI behaviorE2EAsk + warn about maintenance
场景测试类型操作说明
纯函数、解析器、工具函数单元测试立即编写
带有依赖的Effect服务带模拟层的单元测试立即编写
TRPC过程(包含数据库逻辑)TRPC集成测试先询问用户
用户交互流程、UI行为E2E测试询问用户并提醒维护成本

Test File Locations

测试文件存放位置

Code LocationTest Location
packages/X/src/file.ts
packages/X/src/__tests__/file.test.ts
apps/web-app/src/infrastructure/trpc/routers/X.ts
apps/web-app/src/__tests__/X.test.ts
apps/web-app/src/routes/**
apps/web-app/e2e/feature.e2e.ts

代码位置测试文件位置
packages/X/src/file.ts
packages/X/src/__tests__/file.test.ts
apps/web-app/src/infrastructure/trpc/routers/X.ts
apps/web-app/src/__tests__/X.test.ts
apps/web-app/src/routes/**
apps/web-app/e2e/feature.e2e.ts

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; // TestPass123

typescript
// 来自e2eEnv
const testEmail = e2eEnv.E2E_TEST_EMAIL; // test@example.com
const testPassword = e2eEnv.E2E_TEST_PASSWORD; // TestPass123

Commands

测试命令

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 UI
bash
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):**

```bash
bun run vitest run packages/common/src/tests/pagination.test.ts bun run vitest run apps/web-app/src/tests/formatters.test.ts

**错误语法(禁止使用):**

```bash

These 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:
  1. Can this be unit tested? -> Write unit test immediately
  2. Need DB behavior (joins, constraints)? -> Ask: "Does an integration test make sense here?"
  3. Need browser/UI? -> Ask + warn: "E2E tests are expensive to maintain. Is this necessary?"
Never write integration or E2E tests without user confirmation.
在编写任何测试前,请遵循以下步骤:
  1. 是否可以用单元测试覆盖? -> 立即编写单元测试
  2. 需要验证数据库行为(关联查询、约束)? -> 询问用户:"这里需要集成测试吗?"
  3. 需要验证浏览器/UI行为? -> 询问并提醒用户:"E2E测试维护成本很高,这真的有必要吗?"
未经用户确认,请勿编写集成测试或E2E测试。