vitest

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
For E2E tests: Use
prowler-test-ui
skill (Playwright). This skill covers unit/integration tests with Vitest + React Testing Library.
端到端测试说明:请使用
prowler-test-ui
技能(基于Playwright)。 本技能涵盖基于Vitest + React Testing Library的单元/集成测试内容。

Test Structure (REQUIRED)

测试结构(必填)

Use Given/When/Then (AAA) pattern with comments:
typescript
it("should update user name when form is submitted", async () => {
  // Given - Arrange
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  // When - Act
  await user.type(screen.getByLabelText(/name/i), "John");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  // Then - Assert
  expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
});

使用带注释的Given/When/Then(AAA)模式:
typescript
it("should update user name when form is submitted", async () => {
  // Given - 准备阶段
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  // When - 执行阶段
  await user.type(screen.getByLabelText(/name/i), "John");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  // Then - 断言阶段
  expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
});

Describe Block Organization

Describe块组织方式

typescript
describe("ComponentName", () => {
  describe("when [condition]", () => {
    it("should [expected behavior]", () => {});
  });
});
Group by behavior, NOT by method.

typescript
describe("ComponentName", () => {
  describe("when [condition]", () => {
    it("should [expected behavior]", () => {});
  });
});
按行为分组,而非按方法分组。

Query Priority (REQUIRED)

查询方法优先级(必填)

PriorityQueryUse Case
1
getByRole
Buttons, inputs, headings
2
getByLabelText
Form fields
3
getByPlaceholderText
Inputs without label
4
getByText
Static text
5
getByTestId
Last resort only
typescript
// ✅ GOOD
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText(/email/i);

// ❌ BAD
container.querySelector(".btn-primary");

优先级查询方法适用场景
1
getByRole
按钮、输入框、标题
2
getByLabelText
表单字段
3
getByPlaceholderText
无标签的输入框
4
getByText
静态文本
5
getByTestId
仅作为最后手段
typescript
// ✅ 推荐
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText(/email/i);

// ❌ 不推荐
container.querySelector(".btn-primary");

userEvent over fireEvent (REQUIRED)

优先使用userEvent而非fireEvent(必填)

typescript
// ✅ ALWAYS use userEvent
const user = userEvent.setup();
await user.click(button);
await user.type(input, "hello");

// ❌ NEVER use fireEvent for interactions
fireEvent.click(button);

typescript
// ✅ 始终使用userEvent
const user = userEvent.setup();
await user.click(button);
await user.type(input, "hello");

// ❌ 交互操作绝不使用fireEvent
fireEvent.click(button);

Async Testing Patterns

异步测试模式

typescript
// ✅ findBy for elements that appear async
const element = await screen.findByText(/loaded/i);

// ✅ waitFor for assertions
await waitFor(() => {
  expect(screen.getByText(/success/i)).toBeInTheDocument();
});

// ✅ ONE assertion per waitFor
await waitFor(() => expect(mockFn).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());

// ❌ NEVER multiple assertions in waitFor
await waitFor(() => {
  expect(mockFn).toHaveBeenCalled();
  expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures
});

typescript
// ✅ 异步出现的元素使用findBy
const element = await screen.findByText(/loaded/i);

// ✅ 断言使用waitFor
await waitFor(() => {
  expect(screen.getByText(/success/i)).toBeInTheDocument();
});

// ✅ 每个waitFor中仅包含一个断言
await waitFor(() => expect(mockFn).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());

// ❌ 绝不在waitFor中包含多个断言
await waitFor(() => {
  expect(mockFn).toHaveBeenCalled();
  expect(screen.getByText(/done/i)).toBeVisible(); // 失败排查更慢
});

Mocking

模拟(Mocking)

typescript
// Basic mock
const handleClick = vi.fn();

// Mock with return value
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });

// Always clean up
afterEach(() => {
  vi.restoreAllMocks();
});
typescript
// 基础模拟
const handleClick = vi.fn();

// 指定返回值的模拟
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });

// 始终清理模拟
afterEach(() => {
  vi.restoreAllMocks();
});

vi.spyOn vs vi.mock

vi.spyOn vs vi.mock

MethodWhen to Use
vi.spyOn
Observe without replacing (PREFERRED)
vi.mock
Replace entire module (use sparingly)

方法使用场景
vi.spyOn
仅观察不替换(推荐)
vi.mock
替换整个模块(谨慎使用)

Common Matchers

常用匹配器

typescript
// Presence
expect(element).toBeInTheDocument();
expect(element).toBeVisible();

// State
expect(button).toBeDisabled();
expect(input).toHaveValue("text");
expect(checkbox).toBeChecked();

// Content
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveAttribute("href", "/home");

// Functions
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);

typescript
// 存在性
expect(element).toBeInTheDocument();
expect(element).toBeVisible();

// 状态
expect(button).toBeDisabled();
expect(input).toHaveValue("text");
expect(checkbox).toBeChecked();

// 内容
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveAttribute("href", "/home");

// 函数
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);

What NOT to Test

无需测试的内容

typescript
// ❌ Internal state
expect(component.state.isLoading).toBe(true);

// ❌ Third-party libraries
expect(axios.get).toHaveBeenCalled();

// ❌ Static content (unless conditional)
expect(screen.getByText("Welcome")).toBeInTheDocument();

// ✅ User-visible behavior
expect(screen.getByRole("button")).toBeDisabled();

typescript
// ❌ 内部状态
expect(component.state.isLoading).toBe(true);

// ❌ 第三方库
expect(axios.get).toHaveBeenCalled();

// ❌ 静态内容(除非是有条件显示的)
expect(screen.getByText("Welcome")).toBeInTheDocument();

// ✅ 用户可见的行为
expect(screen.getByRole("button")).toBeDisabled();

File Organization

文件组织方式

components/
├── Button/
│   ├── Button.tsx
│   ├── Button.test.tsx    # Co-located
│   └── index.ts

components/
├── Button/
│   ├── Button.tsx
│   ├── Button.test.tsx    # 与组件同目录
│   └── index.ts

Commands

命令

bash
pnpm test                    # Watch mode
pnpm test:run               # Single run
pnpm test:coverage          # With coverage
pnpm test Button            # Filter by name
bash
pnpm test                    # 监听模式
pnpm test:run               # 单次运行
pnpm test:coverage          # 生成覆盖率报告
pnpm test Button            # 按名称过滤测试