Community코딩 & 개발github.com

Naki-404/naki-frontend-testing

Vitest와 React Testing Library를 사용한 프론트엔드 단위 및 컴포넌트 테스트: 동작 중심 테스트, 접근 가능한 쿼리, user-event, MSW를 통한 네트워크 모킹, 훅, 비동기 UI 및 커버리지. React 컴포넌트, 훅 또는 페이지에 대한 테스트를 작성하거나 수정할 때, 또는 테스트가 불안정하거나 느리거나 구현 세부 사항에 결합되어 있을 때 사용하세요.

naki-frontend-testing란 무엇인가요?

이 스킬은 Vitest와 React Testing Library를 활용한 프론트엔드 테스트 작성 방법을 정의합니다. 핵심 원칙은 '사용자가 보고 상호작용하는 것을 테스트하고, 컴포넌트 내부 구현 방식은 테스트하지 않는다'는 것입니다. MSW를 사용해 HTTP 계층에서 네트워크를 모킹하고, `userEvent`로 실제 사용자 상호작용을 시뮬레이션하며, `getByRole`과 같은 접근성 기반 쿼리를 최우선으로 사용합니다. 각 테스트는 하나의 동작만 검증하고, 독립적으로 실행 가능해야 하며, 프로바이더는 `renderWithProviders` 헬퍼를 통해 실제와 유사하게 제공합니다. 또한 비동기 UI, 훅 테스트, 커버리지 가이드라인, 일반적인 실수와 그 해결책까지 포함하여, React 테스트의 전반적인 모범 사례를 체계적으로 제시합니다.

지원 대상~Claude Code~Codex CLI~Cursor
Part ofReact
npx skills add https://github.com/Naki-404/naki-claude-kit/tree/HEAD/kit/skills/naki-frontend-testing

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

프론트엔드 테스팅

사용자가 보고 수행하는 동작을 테스트하고, 컴포넌트가 어떻게 구현되었는지는 테스트하지 마십시오. 루프를 위해 naki-tdd와 함께 사용하고, 전체 브라우저 흐름은 naki-e2e-playwright, 시맨틱 테스트는 naki-accessibility와 함께 사용하세요. 프로젝트에 프로젝트 규칙 스킬이 있다면 이를 따르세요(예: naki-myapp).

사용 시기

  • 새로운 컴포넌트, 훅 또는 페이지에 테스트가 필요한 경우.
  • 리팩터링 후 동작이 변경되지 않았는데 테스트가 깨지는 경우(구현 결합의 신호).
  • 테스트가 불안정하거나, waitFor를 도처에서 사용하거나, 앱의 절반을 모킹하는 경우.

핵심 규칙

  1. 사용자처럼 쿼리하세요. 우선순위: getByRole(name과 함께), getByLabelText, getByPlaceholderText, getByText, getByDisplayValue. 접근 가능한 대안이 없을 때만 getByTestId를 사용하세요.
  2. fireEvent 대신 userEvent로 상호작용하세요. const user = userEvent.setup()으로 생성하고 모든 호출을 await하세요.
  3. 출력 결과를 단언하세요: 보이는 텍스트, 역할, 폼 값, 네트워크 엔드포인트 호출. 상태 변수, 훅 내부, CSS 클래스 이름 또는 컴포넌트 인스턴스 메서드를 단언하지 마세요.
  4. 네트워크를 모킹하고, 모듈은 모킹하지 마세요. MSW 핸들러를 HTTP 계층에서 사용하세요. 진정한 외부 경계(시간, 랜덤, jsdom에 없는 브라우저 API)에 대해서만 모듈을 모킹하세요.
  5. 나중에 나타나는 요소에는 findBy*를 사용하세요. waitFor는 쿼리가 아닌 단언에 사용하고, waitFor 내부에 부수 효과를 절대 넣지 마세요.
  6. 테스트당 하나의 동작, 문장 형태로 이름을 지정하세요: shows an error when the server returns 500.
  7. 각 테스트는 자체 상태를 설정합니다. 순서 의존성 없음; afterEach에서 핸들러, 타이머, 스토리지를 초기화하세요.
  8. 모킹 대신 renderWithProviders 헬퍼를 통해 실제 프로바이더 (라우터, 재시도가 비활성화된 쿼리 클라이언트, 테마)를 제공하세요.
  9. 접근성은 테스트 가능합니다. getByRole("button", { name: "Save" })가 실패하면 실제 접근성 버그를 의미하는 경우가 많습니다. 주요 화면에 선택적으로 vitest-axe 검사를 추가하세요.
  10. 커버리지는 신호일 뿐, 목표가 아닙니다. 중요한 분기(에러 상태, 빈 상태, 권한)를 커버하세요.

쿼리 치트시트

요소쿼리
버튼getByRole("button", { name: /save/i })
링크getByRole("link", { name: "Docs" })
레이블이 있는 텍스트 입력getByRole("textbox", { name: "Email" }) 또는 getByLabelText("Email")
체크박스getByRole("checkbox", { name: "Remember me" })
제목getByRole("heading", { level: 2, name: "Alerts" })
테이블 행getAllByRole("row") 후 within(row)
다이얼로그getByRole("dialog", { name: "Delete alert" })
부재 확인expect(queryByText("Error")).not.toBeInTheDocument()

워크플로우

  1. 동작을 나열하세요: 렌더링 상태(로딩, 빈 상태, 데이터, 에러)와 상호작용.
  2. 컴포넌트가 호출하는 엔드포인트에 대한 MSW 핸들러를 추가하세요; 기본값은 성공.
  3. 동작당 하나의 테스트를 작성하고, 에러 케이스의 경우 server.use(...)로 핸들러를 재정의하세요.
  4. npx vitest run path/to/file.test.tsx를 실행한 다음, 전체 스위트를 실행하세요.
  5. 의도적으로 구현을 한 번 중단(한 줄 주석 처리)하여 테스트가 실패할 수 있는지 확인하세요.

설정

// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());

onUnhandledRequest: "error"는 처리되지 않은 요청이 있을 경우 조용히 넘어가지 않고 명시적으로 실패하도록 만듭니다.

예제

import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";

test("lists alerts and filters by severity", async () => {
  server.use(http.get("/api/alerts", () => HttpResponse.json([
    { id: "a1", title: "Brute force on vpn", severity: "high" },
    { id: "a2", title: "New admin login", severity: "low" },
  ])));
  const user = userEvent.setup();
  render(<AlertList />);

  expect(await screen.findByText("Brute force on vpn")).toBeInTheDocument();
  await user.selectOptions(screen.getByRole("combobox", { name: "Severity" }), "high");
  expect(screen.queryByText("New admin login")).not.toBeInTheDocument();
});

test("shows a retry button when loading fails", async () => {
  server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
  render(<AlertList />);
  expect(await screen.findByRole("button", { name: /retry/i })).toBeInTheDocument();
});

훅과 시간

  • 훅을 사용하는 작은 컴포넌트를 통해 테스트하거나, UI가 없는 경우 renderHook을 사용하여 테스트하세요.
  • 가짜 타이머: vi.useFakeTimers()와 userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); afterEach에서 vi.useRealTimers()로 복원하세요.
  • vi.setSystemTime(new Date("2026-01-01T00:00:00Z"))로 날짜를 고정하여 스냅샷과 레이블이 안정적이도록 하세요.

체크리스트

  • 쿼리가 역할 또는 레이블 기반입니다.
  • 모든 userEvent 호출이 await됩니다.
  • 네트워크가 MSW로 모킹되었으며, 처리되지 않은 요청은 실패합니다.
  • 로딩, 에러 및 빈 상태가 커버되었습니다.
  • 내부 구현 또는 클래스 이름에 대한 단언이 없습니다.
  • 테스트가 단독으로 실행되든 어떤 순서로든 통과합니다.
  • 출력에 act(...) 경고가 없습니다.

일반적인 실수

실수해결책
모든 곳에서 getByTestId 사용역할/레이블 쿼리; 누락된 경우 컴포넌트에 레이블 추가
await waitFor(() => getByText("x"))await findByText("x")
타이핑에 fireEvent.change 사용await user.type(input, "text")
useAlerts 훅을 직접 모킹HTTP 엔드포인트 모킹
전체 페이지의 큰 스냅샷중요한 부분에 대한 명시적 단언
재시도가 활성화된 쿼리 클라이언트로 인한 느린 에러 테스트테스트 프로바이더에서 retry: false
무시된 act 경고findBy*로 비동기 UI 업데이트를 await

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

관련 스킬