test-generator

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test Generator

测试生成器

Before generating any output, read
config/defaults.md
and adapt all patterns, imports, and code examples to the user's configured stack.
在生成任何输出之前,请阅读
config/defaults.md
,并根据用户的配置栈调整所有模式、导入语句和代码示例。

Process

流程

  1. Read the source file to understand its exports, dependencies, and behavior.
  2. Identify the source type: API route, utility function, React component, or hook.
  3. Generate test cases covering: happy path, edge cases, error handling, boundary values.
  4. Write the test file using Vitest syntax (fall back to Jest if the project uses it).
  5. Include proper mocking, setup/teardown, and descriptive test names.
  1. 读取源文件以理解其导出内容、依赖项和行为。
  2. 识别源文件类型:API路由、工具函数、React组件或Hook。
  3. 生成涵盖以下场景的测试用例:正常路径、边缘情况、错误处理、边界值。
  4. 使用Vitest语法编写测试文件(如果项目使用Jest则回退到Jest语法)。
  5. 包含适当的模拟、初始化/清理操作以及描述性的测试名称。

File Naming and Placement

文件命名与存放规则

  • Place test file adjacent to source:
    foo.ts
    foo.test.ts
    ,
    Foo.tsx
    Foo.test.tsx
    .
  • If the project uses a
    __tests__/
    directory convention, follow that instead.
  • For API route tests:
    app/api/users/route.ts
    app/api/users/route.test.ts
    .
  • 将测试文件与源文件放在同一目录下:
    foo.ts
    foo.test.ts
    Foo.tsx
    Foo.test.tsx
  • 如果项目使用
    __tests__/
    目录约定,则遵循该约定。
  • 对于API路由测试:
    app/api/users/route.ts
    app/api/users/route.test.ts

Testing API Routes

API路由测试

Analyze the route handler and generate tests for each HTTP method.
typescript
// Source: app/api/users/route.ts (POST handler)

// Test: app/api/users/route.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "./route";
import { prisma } from "@/lib/prisma";

vi.mock("@/lib/prisma", () => ({
  prisma: {
    user: {
      create: vi.fn(),
      findUnique: vi.fn(),
    },
  },
}));

describe("POST /api/users", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should create a user and return 201", async () => {
    const mockUser = { id: "1", email: "test@example.com", name: "Test" };
    vi.mocked(prisma.user.create).mockResolvedValue(mockUser);

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "test@example.com", name: "Test" }),
    });

    const response = await POST(request);
    const data = await response.json();

    expect(response.status).toBe(201);
    expect(data).toEqual(mockUser);
  });

  it("should return 400 when email is missing", async () => {
    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(400);
  });

  it("should return 409 when email already exists", async () => {
    vi.mocked(prisma.user.create).mockRejectedValue(
      new Error("Unique constraint failed on the fields: (`email`)")
    );

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "taken@example.com", name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(409);
  });

  it("should return 500 on unexpected error", async () => {
    vi.mocked(prisma.user.create).mockRejectedValue(new Error("DB down"));

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "test@example.com", name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(500);
  });
});
分析路由处理器,为每个HTTP方法生成测试用例。
typescript
// Source: app/api/users/route.ts (POST handler)

// Test: app/api/users/route.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "./route";
import { prisma } from "@/lib/prisma";

vi.mock("@/lib/prisma", () => ({
  prisma: {
    user: {
      create: vi.fn(),
      findUnique: vi.fn(),
    },
  },
}));

describe("POST /api/users", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should create a user and return 201", async () => {
    const mockUser = { id: "1", email: "test@example.com", name: "Test" };
    vi.mocked(prisma.user.create).mockResolvedValue(mockUser);

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "test@example.com", name: "Test" }),
    });

    const response = await POST(request);
    const data = await response.json();

    expect(response.status).toBe(201);
    expect(data).toEqual(mockUser);
  });

  it("should return 400 when email is missing", async () => {
    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(400);
  });

  it("should return 409 when email already exists", async () => {
    vi.mocked(prisma.user.create).mockRejectedValue(
      new Error("Unique constraint failed on the fields: (`email`)")
    );

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "taken@example.com", name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(409);
  });

  it("should return 500 on unexpected error", async () => {
    vi.mocked(prisma.user.create).mockRejectedValue(new Error("DB down"));

    const request = new Request("http://localhost/api/users", {
      method: "POST",
      body: JSON.stringify({ email: "test@example.com", name: "Test" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(500);
  });
});

API Route Test Checklist

API路由测试检查清单

  • Each HTTP method has at least one happy path test
  • Invalid request body returns 400
  • Missing/invalid auth returns 401
  • Forbidden access returns 403
  • Resource not found returns 404
  • Duplicate/conflict returns 409
  • Unexpected errors return 500 (not leak stack traces)
  • 每个HTTP方法至少有一个正常路径测试
  • 无效请求体返回400
  • 缺失/无效的认证信息返回401
  • 禁止访问返回403
  • 资源不存在返回404
  • 重复/冲突返回409
  • 意外错误返回500(不泄露堆栈跟踪)

Testing Utility Functions

工具函数测试

Focus on pure logic, null/undefined handling, and thrown errors.
typescript
// Source: lib/utils/slugify.ts

// Test: lib/utils/slugify.test.ts
import { describe, it, expect } from "vitest";
import { slugify } from "./slugify";

describe("slugify", () => {
  it("should convert a simple string to slug", () => {
    expect(slugify("Hello World")).toBe("hello-world");
  });

  it("should handle special characters", () => {
    expect(slugify("Héllo & Wörld!")).toBe("hello-world");
  });

  it("should collapse multiple hyphens", () => {
    expect(slugify("foo---bar")).toBe("foo-bar");
  });

  it("should trim leading and trailing hyphens", () => {
    expect(slugify("-hello-")).toBe("hello");
  });

  it("should return empty string for empty input", () => {
    expect(slugify("")).toBe("");
  });

  it("should handle null/undefined gracefully", () => {
    expect(slugify(null as unknown as string)).toBe("");
  });
});
聚焦纯逻辑、null/undefined处理以及抛出的错误。
typescript
// Source: lib/utils/slugify.ts

// Test: lib/utils/slugify.test.ts
import { describe, it, expect } from "vitest";
import { slugify } from "./slugify";

describe("slugify", () => {
  it("should convert a simple string to slug", () => {
    expect(slugify("Hello World")).toBe("hello-world");
  });

  it("should handle special characters", () => {
    expect(slugify("Héllo & Wörld!")).toBe("hello-world");
  });

  it("should collapse multiple hyphens", () => {
    expect(slugify("foo---bar")).toBe("foo-bar");
  });

  it("should trim leading and trailing hyphens", () => {
    expect(slugify("-hello-")).toBe("hello");
  });

  it("should return empty string for empty input", () => {
    expect(slugify("")).toBe("");
  });

  it("should handle null/undefined gracefully", () => {
    expect(slugify(null as unknown as string)).toBe("");
  });
});

Testing React Components

React组件测试

Use React Testing Library. Prefer
getByRole
over
getByTestId
.
tsx
// Test: components/LoginForm.test.tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

describe("LoginForm", () => {
  it("should render email and password fields", () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    expect(screen.getByRole("textbox", { name: /email/i })).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  });

  it("should call onSubmit with form values", async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    await user.type(screen.getByRole("textbox", { name: /email/i }), "a@b.com");
    await user.type(screen.getByLabelText(/password/i), "secret123");
    await user.click(screen.getByRole("button", { name: /sign in/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: "a@b.com",
      password: "secret123",
    });
  });

  it("should show validation error for invalid email", async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.type(screen.getByRole("textbox", { name: /email/i }), "bad");
    await user.click(screen.getByRole("button", { name: /sign in/i }));

    expect(screen.getByRole("alert")).toHaveTextContent(/valid email/i);
  });

  it("should disable submit button while loading", () => {
    render(<LoginForm onSubmit={vi.fn()} isLoading />);

    expect(screen.getByRole("button", { name: /sign in/i })).toBeDisabled();
  });
});
使用React Testing Library。优先使用
getByRole
而非
getByTestId
tsx
// Test: components/LoginForm.test.tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

describe("LoginForm", () => {
  it("should render email and password fields", () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    expect(screen.getByRole("textbox", { name: /email/i })).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  });

  it("should call onSubmit with form values", async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    await user.type(screen.getByRole("textbox", { name: /email/i }), "a@b.com");
    await user.type(screen.getByLabelText(/password/i), "secret123");
    await user.click(screen.getByRole("button", { name: /sign in/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: "a@b.com",
      password: "secret123",
    });
  });

  it("should show validation error for invalid email", async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.type(screen.getByRole("textbox", { name: /email/i }), "bad");
    await user.click(screen.getByRole("button", { name: /sign in/i }));

    expect(screen.getByRole("alert")).toHaveTextContent(/valid email/i);
  });

  it("should disable submit button while loading", () => {
    render(<LoginForm onSubmit={vi.fn()} isLoading />);

    expect(screen.getByRole("button", { name: /sign in/i })).toBeDisabled();
  });
});

Component Test Checklist

组件测试检查清单

  • Renders without crashing
  • Displays correct content based on props
  • Interactive elements respond to user events
  • Conditional rendering works for all states (loading, error, empty, success)
  • Accessible: interactive elements have roles and labels
  • Form submission calls handler with correct values
  • Error states display properly
  • 渲染无崩溃
  • 根据props显示正确内容
  • 交互元素响应用户事件
  • 条件渲染在所有状态下正常工作(加载、错误、空数据、成功)
  • 可访问性:交互元素具备角色和标签
  • 表单提交时调用处理器并传入正确值
  • 错误状态正确显示

Testing Hooks

Hook测试

Wrap custom hooks with
renderHook
from React Testing Library.
typescript
// Test: hooks/useDebounce.test.ts
import { describe, it, expect, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useDebounce } from "./useDebounce";

describe("useDebounce", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("should return initial value immediately", () => {
    const { result } = renderHook(() => useDebounce("hello", 500));
    expect(result.current).toBe("hello");
  });

  it("should debounce value updates", () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: "hello" } }
    );

    rerender({ value: "world" });
    expect(result.current).toBe("hello"); // not yet updated

    act(() => { vi.advanceTimersByTime(500); });
    expect(result.current).toBe("world"); // now updated
  });

  it("should cancel previous timer on rapid updates", () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: "a" } }
    );

    rerender({ value: "b" });
    act(() => { vi.advanceTimersByTime(300); });
    rerender({ value: "c" });
    act(() => { vi.advanceTimersByTime(500); });

    expect(result.current).toBe("c"); // skipped "b"
  });
});
使用React Testing Library的
renderHook
包裹自定义Hook。
typescript
// Test: hooks/useDebounce.test.ts
import { describe, it, expect, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useDebounce } from "./useDebounce";

describe("useDebounce", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("should return initial value immediately", () => {
    const { result } = renderHook(() => useDebounce("hello", 500));
    expect(result.current).toBe("hello");
  });

  it("should debounce value updates", () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: "hello" } }
    );

    rerender({ value: "world" });
    expect(result.current).toBe("hello"); // not yet updated

    act(() => { vi.advanceTimersByTime(500); });
    expect(result.current).toBe("world"); // now updated
  });

  it("should cancel previous timer on rapid updates", () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: "a" } }
    );

    rerender({ value: "b" });
    act(() => { vi.advanceTimersByTime(300); });
    rerender({ value: "c" });
    act(() => { vi.advanceTimersByTime(500); });

    expect(result.current).toBe("c"); // skipped "b"
  });
});

Mocking Patterns

模拟模式

Mock a module

模拟模块

typescript
vi.mock("@/lib/prisma", () => ({
  prisma: { user: { findMany: vi.fn() } },
}));
typescript
vi.mock("@/lib/prisma", () => ({
  prisma: { user: { findMany: vi.fn() } },
}));

Mock next/navigation

模拟next/navigation

typescript
vi.mock("next/navigation", () => ({
  useRouter: () => ({ push: vi.fn(), back: vi.fn() }),
  useSearchParams: () => new URLSearchParams("q=test"),
  usePathname: () => "/dashboard",
}));
typescript
vi.mock("next/navigation", () => ({
  useRouter: () => ({ push: vi.fn(), back: vi.fn() }),
  useSearchParams: () => new URLSearchParams("q=test"),
  usePathname: () => "/dashboard",
}));

Mock fetch

模拟fetch

typescript
const mockFetch = vi.fn();
global.fetch = mockFetch;

mockFetch.mockResolvedValueOnce({
  ok: true,
  json: () => Promise.resolve({ data: "test" }),
});
typescript
const mockFetch = vi.fn();
global.fetch = mockFetch;

mockFetch.mockResolvedValueOnce({
  ok: true,
  json: () => Promise.resolve({ data: "test" }),
});

Test Naming Convention

测试命名规范

Use descriptive names following the pattern:
should [expected behavior] when [condition]
.
  • should return 401 when auth token is missing
  • should render loading spinner when data is fetching
  • should throw TypeError when input is not a string
使用描述性名称,遵循模式:
should [预期行为] when [条件]
  • should return 401 when auth token is missing
  • should render loading spinner when data is fetching
  • should throw TypeError when input is not a string

Output Format

输出格式

undefined
undefined

Generated Tests

生成的测试用例

Source:
path/to/source.ts
Test file:
path/to/source.test.ts
Framework: Vitest
源文件:
path/to/source.ts
测试文件:
path/to/source.test.ts
测试框架: Vitest

Test Cases

测试用例列表

#TestCategory
1should ...Happy path
2should ...Edge case
3should ...Error handling
[Generated code block]
#测试内容分类
1should ...正常路径
2should ...边缘情况
3should ...错误处理
[生成的代码块]

Coverage Notes

覆盖率说明

  • [Any areas that need additional manual test cases]
undefined
  • [任何需要额外手动测试用例的区域]
undefined

Verification Loop

验证循环

After generating tests, run them with the project's test runner. If any test fails due to a generation error (not a legitimate bug), analyze the failure, fix the test, and re-run. Repeat up to 3 times. If a test fails because it caught a real bug in the source code, flag it clearly in the output:
REAL BUG FOUND: [description]
. Only deliver tests that either pass or explicitly flag real bugs.
生成测试后,使用项目的测试运行器执行测试。如果任何测试因生成错误(而非合法bug)失败,请分析失败原因、修复测试并重新运行。重复此步骤最多3次。如果测试失败是因为捕获到源文件中的真实bug,请在输出中明确标记:
发现真实BUG: [描述]
。仅交付可通过或明确标记真实bug的测试用例。

Reference

参考

See references/testing-patterns.md for Vitest setup, Prisma mocking, and MSW patterns.
请查看references/testing-patterns.md获取Vitest配置、Prisma模拟及MSW模式相关内容。