fe-test
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFE Test Generation
FE测试生成
$ARGUMENTS通过传递的文件进行分析,生成合适的测试代码。
$ARGUMENTS테스트 생성 절차
测试生成流程
- 대상 파일 분석: 파일을 읽고 export된 함수/컴포넌트/훅을 파악한다
- 테스트 유형 결정: 파일 유형에 따라 적절한 테스트 전략을 선택한다
- 테스트 파일 생성: co-location 원칙에 따라 동일 디렉토리에 생성
.test.ts(x) - 실행 확인: 사용자에게 실행을 안내한다
vitest run
- 目标文件分析:读取文件并识别导出的函数/组件/Hook
- 测试类型确定:根据文件类型选择合适的测试策略
- 测试文件生成:按照co-location原则在同一目录下创建文件
.test.ts(x) - 执行确认:引导用户执行
vitest run
파일 유형별 테스트 전략
按文件类型划分的测试策略
| 파일 유형 | 테스트 도구 | 테스트 초점 |
|---|---|---|
| 유틸리티 함수 | Vitest | 입출력, 엣지 케이스, 에러 |
| 커스텀 훅 | renderHook | 상태 변화, 반환값, 사이드이펙트 |
| UI 컴포넌트 | RTL + Vitest | 렌더링, 인터랙션, 접근성 |
| 폼 컴포넌트 | RTL + user-event | 입력, 유효성 검사, 제출 |
| API 호출 | MSW + Vitest | 요청/응답, 에러 처리, 로딩 상태 |
| 페이지 | RTL | 통합 렌더링, 라우팅, 데이터 표시 |
| Zustand 스토어 | Vitest | 상태 변경, 액션, 셀렉터 |
| 文件类型 | 测试工具 | 测试重点 |
|---|---|---|
| 工具函数 | Vitest | 输入输出、边界情况、错误处理 |
| 自定义Hook | renderHook | 状态变化、返回值、副作用 |
| UI组件 | RTL + Vitest | 渲染、交互、可访问性 |
| 表单组件 | RTL + user-event | 输入、验证、提交 |
| API调用 | MSW + Vitest | 请求/响应、错误处理、加载状态 |
| 页面 | RTL | 集成渲染、路由、数据展示 |
| Zustand Store | Vitest | 状态变更、动作、选择器 |
테스트 코드 컨벤션
测试代码规范
기본 구조
基本结构
typescript
import { describe, expect, it, vi, beforeEach } from "vitest";
describe("[테스트 대상]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("[기능/메서드]", () => {
it("[기대 동작을 서술]", () => {
// Arrange
// Act
// Assert
});
});
});typescript
import { describe, expect, it, vi, beforeEach } from "vitest";
describe("[测试目标]", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("[功能/方法]", () => {
it("[描述预期行为]", () => {
// Arrange(准备)
// Act(执行)
// Assert(断言)
});
});
});유틸리티 함수 테스트
工具函数测试
typescript
import { describe, expect, it } from "vitest";
import { formatCurrency } from "./formatCurrency";
describe("formatCurrency", () => {
it("formats number with comma separators", () => {
expect(formatCurrency(1000)).toBe("₩1,000");
});
it("handles zero", () => {
expect(formatCurrency(0)).toBe("₩0");
});
it("handles negative numbers", () => {
expect(formatCurrency(-500)).toBe("-₩500");
});
it("rounds decimal places", () => {
expect(formatCurrency(99.999)).toBe("₩100");
});
});typescript
import { describe, expect, it } from "vitest";
import { formatCurrency } from "./formatCurrency";
describe("formatCurrency", () => {
it("formats number with comma separators", () => {
expect(formatCurrency(1000)).toBe("₩1,000");
});
it("handles zero", () => {
expect(formatCurrency(0)).toBe("₩0");
});
it("handles negative numbers", () => {
expect(formatCurrency(-500)).toBe("-₩500");
});
it("rounds decimal places", () => {
expect(formatCurrency(99.999)).toBe("₩100");
});
});컴포넌트 테스트
组件测试
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("renders with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
it("applies variant classes", () => {
render(<Button variant="destructive">Delete</Button>);
expect(screen.getByRole("button")).toHaveClass("bg-destructive");
});
});tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("renders with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
it("applies variant classes", () => {
render(<Button variant="destructive">Delete</Button>);
expect(screen.getByRole("button")).toHaveClass("bg-destructive");
});
});커스텀 훅 테스트
自定义Hook测试
typescript
import { renderHook, act } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
it("initializes with default value", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it("initializes with provided value", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it("increments count", () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it("decrements count", () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.decrement());
expect(result.current.count).toBe(4);
});
});typescript
import { renderHook, act } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
it("initializes with default value", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it("initializes with provided value", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it("increments count", () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it("decrements count", () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.decrement());
expect(result.current.count).toBe(4);
});
});폼 테스트
表单测试
tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { LoginForm } from "./LoginForm";
describe("LoginForm", () => {
const mockSubmit = vi.fn();
it("submits with valid data", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
it("shows validation error for invalid email", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText("Email"), "invalid");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/valid email/i)).toBeInTheDocument();
expect(mockSubmit).not.toHaveBeenCalled();
});
});tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { LoginForm } from "./LoginForm";
describe("LoginForm", () => {
const mockSubmit = vi.fn();
it("submits with valid data", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
it("shows validation error for invalid email", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText("Email"), "invalid");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/valid email/i)).toBeInTheDocument();
expect(mockSubmit).not.toHaveBeenCalled();
});
});API 호출 테스트 (MSW)
API调用测试(MSW)
typescript
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { fetchUsers } from "./api";
const server = setupServer(
http.get("/api/users", () => {
return HttpResponse.json([
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("fetchUsers", () => {
it("returns user list", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0].name).toBe("Alice");
});
it("handles server error", async () => {
server.use(
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
})
);
await expect(fetchUsers()).rejects.toThrow();
});
});typescript
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { fetchUsers } from "./api";
const server = setupServer(
http.get("/api/users", () => {
return HttpResponse.json([
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("fetchUsers", () => {
it("returns user list", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0].name).toBe("Alice");
});
it("handles server error", async () => {
server.use(
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
})
);
await expect(fetchUsers()).rejects.toThrow();
});
});Zustand 스토어 테스트
Zustand Store测试
typescript
import { describe, expect, it, beforeEach } from "vitest";
import { useCartStore } from "./cartStore";
describe("cartStore", () => {
beforeEach(() => {
useCartStore.setState({ items: [], total: 0 });
});
it("adds item to cart", () => {
const { addItem } = useCartStore.getState();
addItem({ id: "1", name: "Product", price: 100 });
const { items } = useCartStore.getState();
expect(items).toHaveLength(1);
expect(items[0].name).toBe("Product");
});
it("calculates total", () => {
const { addItem } = useCartStore.getState();
addItem({ id: "1", name: "A", price: 100 });
addItem({ id: "2", name: "B", price: 200 });
expect(useCartStore.getState().total).toBe(300);
});
});typescript
import { describe, expect, it, beforeEach } from "vitest";
import { useCartStore } from "./cartStore";
describe("cartStore", () => {
beforeEach(() => {
useCartStore.setState({ items: [], total: 0 });
});
it("adds item to cart", () => {
const { addItem } = useCartStore.getState();
addItem({ id: "1", name: "Product", price: 100 });
const { items } = useCartStore.getState();
expect(items).toHaveLength(1);
expect(items[0].name).toBe("Product");
});
it("calculates total", () => {
const { addItem } = useCartStore.getState();
addItem({ id: "1", name: "A", price: 100 });
addItem({ id: "2", name: "B", price: 200 });
expect(useCartStore.getState().total).toBe(300);
});
});테스트 작성 원칙
测试编写原则
- 사용자 관점으로 테스트: 구현 상세가 아닌 동작을 테스트한다
- 접근성 쿼리 우선: >
getByRole>getByLabelText>getByTextgetByTestId - AAA 패턴: Arrange → Act → Assert
- 단일 검증: 하나의 에서 하나의 동작만 검증
it - Mocking 최소화: 외부 의존성만 mock, 내부 구현은 mock하지 않음
- 엣지 케이스 포함: 빈 값, null, 에러, 경계값 테스트
- 从用户视角测试:测试行为而非实现细节
- 优先使用可访问性查询:>
getByRole>getByLabelText>getByTextgetByTestId - AAA模式:Arrange(准备)→ Act(执行)→ Assert(断言)
- 单一验证:每个仅验证一个行为
it - 最小化Mock:仅Mock外部依赖,不Mock内部实现
- 包含边界情况:测试空值、null、错误、边界值
실행 규칙
执行规则
- 인자가 없으면 사용자에게 테스트 대상을 질문한다
- 대상 파일을 먼저 읽고, export된 항목을 파악한다
- 기존 테스트 파일이 있으면 읽고, 누락된 케이스를 추가한다
- 프로젝트의 테스트 설정(vitest.config, setup 파일)을 확인하고 맞춘다
- matchers 사용 가능 여부 확인
@testing-library/jest-dom
- 若无参数,则询问用户测试目标
- 先读取目标文件,识别导出的内容
- 若存在现有测试文件,读取并补充缺失的测试用例
- 检查项目的测试配置(vitest.config、setup文件)并适配
- 确认是否可使用匹配器
@testing-library/jest-dom