vitest
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFor E2E tests: Useskill (Playwright). This skill covers unit/integration tests with Vitest + React Testing Library.prowler-test-ui
端到端测试说明:请使用技能(基于Playwright)。 本技能涵盖基于Vitest + React Testing Library的单元/集成测试内容。prowler-test-ui
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)
查询方法优先级(必填)
| Priority | Query | Use Case |
|---|---|---|
| 1 | | Buttons, inputs, headings |
| 2 | | Form fields |
| 3 | | Inputs without label |
| 4 | | Static text |
| 5 | | Last resort only |
typescript
// ✅ GOOD
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText(/email/i);
// ❌ BAD
container.querySelector(".btn-primary");| 优先级 | 查询方法 | 适用场景 |
|---|---|---|
| 1 | | 按钮、输入框、标题 |
| 2 | | 表单字段 |
| 3 | | 无标签的输入框 |
| 4 | | 静态文本 |
| 5 | | 仅作为最后手段 |
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
| Method | When to Use |
|---|---|
| Observe without replacing (PREFERRED) |
| Replace entire module (use sparingly) |
| 方法 | 使用场景 |
|---|---|
| 仅观察不替换(推荐) |
| 替换整个模块(谨慎使用) |
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.tscomponents/
├── Button/
│ ├── Button.tsx
│ ├── Button.test.tsx # 与组件同目录
│ └── index.tsCommands
命令
bash
pnpm test # Watch mode
pnpm test:run # Single run
pnpm test:coverage # With coverage
pnpm test Button # Filter by namebash
pnpm test # 监听模式
pnpm test:run # 单次运行
pnpm test:coverage # 生成覆盖率报告
pnpm test Button # 按名称过滤测试