fe-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

FE Test Generation

FE测试生成

$ARGUMENTS
로 전달된 파일을 분석하고 적절한 테스트 코드를 생성한다.
通过
$ARGUMENTS
传递的文件进行分析,生成合适的测试代码。

테스트 생성 절차

测试生成流程

  1. 대상 파일 분석: 파일을 읽고 export된 함수/컴포넌트/훅을 파악한다
  2. 테스트 유형 결정: 파일 유형에 따라 적절한 테스트 전략을 선택한다
  3. 테스트 파일 생성: co-location 원칙에 따라 동일 디렉토리에
    .test.ts(x)
    생성
  4. 실행 확인: 사용자에게
    vitest run
    실행을 안내한다
  1. 目标文件分析:读取文件并识别导出的函数/组件/Hook
  2. 测试类型确定:根据文件类型选择合适的测试策略
  3. 测试文件生成:按照co-location原则在同一目录下创建
    .test.ts(x)
    文件
  4. 执行确认:引导用户执行
    vitest run

파일 유형별 테스트 전략

按文件类型划分的测试策略

파일 유형테스트 도구테스트 초점
유틸리티 함수Vitest입출력, 엣지 케이스, 에러
커스텀 훅renderHook상태 변화, 반환값, 사이드이펙트
UI 컴포넌트RTL + Vitest렌더링, 인터랙션, 접근성
폼 컴포넌트RTL + user-event입력, 유효성 검사, 제출
API 호출MSW + Vitest요청/응답, 에러 처리, 로딩 상태
페이지RTL통합 렌더링, 라우팅, 데이터 표시
Zustand 스토어Vitest상태 변경, 액션, 셀렉터
文件类型测试工具测试重点
工具函数Vitest输入输出、边界情况、错误处理
自定义HookrenderHook状态变化、返回值、副作用
UI组件RTL + Vitest渲染、交互、可访问性
表单组件RTL + user-event输入、验证、提交
API调用MSW + Vitest请求/响应、错误处理、加载状态
页面RTL集成渲染、路由、数据展示
Zustand StoreVitest状态变更、动作、选择器

테스트 코드 컨벤션

测试代码规范

기본 구조

基本结构

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);
  });
});

테스트 작성 원칙

测试编写原则

  1. 사용자 관점으로 테스트: 구현 상세가 아닌 동작을 테스트한다
  2. 접근성 쿼리 우선:
    getByRole
    >
    getByLabelText
    >
    getByText
    >
    getByTestId
  3. AAA 패턴: Arrange → Act → Assert
  4. 단일 검증: 하나의
    it
    에서 하나의 동작만 검증
  5. Mocking 최소화: 외부 의존성만 mock, 내부 구현은 mock하지 않음
  6. 엣지 케이스 포함: 빈 값, null, 에러, 경계값 테스트
  1. 从用户视角测试:测试行为而非实现细节
  2. 优先使用可访问性查询
    getByRole
    >
    getByLabelText
    >
    getByText
    >
    getByTestId
  3. AAA模式:Arrange(准备)→ Act(执行)→ Assert(断言)
  4. 单一验证:每个
    it
    仅验证一个行为
  5. 最小化Mock:仅Mock外部依赖,不Mock内部实现
  6. 包含边界情况:测试空值、null、错误、边界值

실행 규칙

执行规则

  1. 인자가 없으면 사용자에게 테스트 대상을 질문한다
  2. 대상 파일을 먼저 읽고, export된 항목을 파악한다
  3. 기존 테스트 파일이 있으면 읽고, 누락된 케이스를 추가한다
  4. 프로젝트의 테스트 설정(vitest.config, setup 파일)을 확인하고 맞춘다
  5. @testing-library/jest-dom
    matchers 사용 가능 여부 확인
  1. 若无参数,则询问用户测试目标
  2. 先读取目标文件,识别导出的内容
  3. 若存在现有测试文件,读取并补充缺失的测试用例
  4. 检查项目的测试配置(vitest.config、setup文件)并适配
  5. 确认是否可使用
    @testing-library/jest-dom
    匹配器