fe-test

📁 ingpdw/pdw-fe-dev-tool 📅 6 days ago
1
总安装量
1
周安装量
#48042
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-test

Agent 安装分布

mcpjam 1
claude-code 1
replit 1
junie 1
zencoder 1

Skill 文档

FE Test Generation

$ARGUMENTS로 전달된 파일을 분석하고 적절한 테스트 코드를 생성한다.

테스트 생성 절차

  1. 대상 파일 분석: 파일을 읽고 export된 함수/컴포넌트/훅을 파악한다
  2. 테스트 유형 결정: 파일 유형에 따라 적절한 테스트 전략을 선택한다
  3. 테스트 파일 생성: co-location 원칙에 따라 동일 디렉토리에 .test.ts(x) 생성
  4. 실행 확인: 사용자에게 vitest run 실행을 안내한다

파일 유형별 테스트 전략

파일 유형 테스트 도구 테스트 초점
유틸리티 함수 Vitest 입출력, 엣지 케이스, 에러
커스텀 훅 renderHook 상태 변화, 반환값, 사이드이펙트
UI 컴포넌트 RTL + Vitest 렌더링, 인터랙션, 접근성
폼 컴포넌트 RTL + user-event 입력, 유효성 검사, 제출
API 호출 MSW + Vitest 요청/응답, 에러 처리, 로딩 상태
페이지 RTL 통합 렌더링, 라우팅, 데이터 표시
Zustand 스토어 Vitest 상태 변경, 액션, 셀렉터

테스트 코드 컨벤션

기본 구조

import { describe, expect, it, vi, beforeEach } from "vitest";

describe("[테스트 대상]", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("[기능/메서드]", () => {
    it("[기대 동작을 서술]", () => {
      // Arrange
      // Act
      // Assert
    });
  });
});

유틸리티 함수 테스트

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

컴포넌트 테스트

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

커스텀 훅 테스트

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

폼 테스트

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)

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 스토어 테스트

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. 대상 파일을 먼저 읽고, export된 항목을 파악한다
  3. 기존 테스트 파일이 있으면 읽고, 누락된 케이스를 추가한다
  4. 프로젝트의 테스트 설정(vitest.config, setup 파일)을 확인하고 맞춘다
  5. @testing-library/jest-dom matchers 사용 가능 여부 확인