Communitygithub.com

React Testing

React Testing Library, Vitest/Jest, MSW를 활용한 React 컴포넌트 테스트, 접근성 검증(axe), 그리고 컴포넌트 테스트와 Playwright/Cypress E2E 테스트 간의 결정 경계를 다룹니다. React 컴포넌트, 훅, 페이지의 테스트 작성 또는 수정 시 사용하세요.

React Testing란 무엇인가요?

이 스킬은 React Testing Library, Vitest/Jest, MSW, axe를 활용한 포괄적인 React 테스트 가이드를 제공합니다. 사용자 중심의 동작 기반 테스트, 커스텀 훅 테스트, 접근성 어설션, 네트워크 수준 모킹을 다루며, JSDOM이 지원하지 않는 기능(레이아웃, 브라우저 API)은 Playwright/Cypress 컴포넌트 테스트로 전환하는 명확한 판단 기준을 제시합니다. 쿼리 우선순위(접근성 > 의미론적 > 테스트 ID), userEvent를 통한 실제 사용자 상호작용 시뮬레이션, MSW를 이용한 네트워크 레이어 모킹 등 실용적인 패턴을 예제 코드와 함께 설명합니다. 또한 스냅샷 테스트 지양, act() 경고 무시 금지 등 반드시 피해야 할 안티패턴을 강조하고, TDD 워크플로우와 Vitest/Jest 명령어를 포함하여 실무에서 바로 적용 가능한 지침을 제공합니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/react-testing

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

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

문서

React Testing은(는) 무엇을 하나요?

동작 중심의 컴포넌트 테스트, 커스텀 훅 테스트, 접근성 어설션, 네트워크 레벨 모킹을 위한 포괄적인 React 테스트 패턴입니다.

활성화 시점

  • React 컴포넌트, 커스텀 훅, 페이지에 대한 테스트를 작성할 때
  • 레거시 미테스트 컴포넌트에 테스트 커버리지를 추가할 때
  • Enzyme 또는 클래스 컴포넌트 패턴에서 React Testing Library로 마이그레이션할 때
  • 새 React 프로젝트에 Vitest 또는 Jest를 설정할 때
  • 테스트에서 HTTP 요청을 모킹할 때
  • 접근성 위반 사항을 어설션할 때
  • 어떤 테스트가 RTL에 속하고 어떤 것이 Playwright Component Testing 또는 전체 E2E에 속하는지 결정할 때

핵심 원칙

사용자가 보고 상호 작용하는 것을 테스트하고, 구현 세부 사항은 테스트하지 마십시오.

테스트는 다음을 수행해야 합니다:

  • 프로덕션에서 사용하는 것과 동일한 프로바이더와 함께 컴포넌트를 렌더링합니다.
  • 접근 가능한 쿼리(getByRole, getByLabelText)와 userEvent를 사용하여 상호 작용합니다.
  • 표시되는 출력과 관찰 가능한 부수 효과(콜백 호출, 요청 전송)를 어설션합니다.

테스트는 다음을 수행해서는 안 됩니다:

  • 컴포넌트 상태, 자식에게 전달된 props, 또는 어떤 훅이 호출되었는지 검사합니다.
  • React 자체 또는 프레임워크 훅을 모킹합니다.
  • 사용자에게 영향을 미치는 것 이상으로 렌더링 횟수나 DOM 구조를 어설션합니다.

라이브러리 선택

러너시기참고
VitestVite, Remix, 최신 설정더 빠름, 네이티브 ESM, Jest 호환 API
JestNext.js, CRA, 기존 저장소많은 React 프로젝트의 기본값
Playwright Component Testing실제 브라우저 엔진 필요 시JSDOM에 필요한 기능이 없을 때 사용
Cypress Component Testing실제 브라우저, Cypress 이미 사용 중인 경우Playwright CT의 대안

하나를 선택하세요. 명확한 레인 분리가 없는 한 동일한 저장소에서 RTL + Vitest와 Playwright CT를 함께 실행하지 마십시오.

쿼리 우선순위

React Testing Library는 세 가지 계층의 쿼리를 제공합니다. 위에서부터 아래로 사용하십시오:

  1. 모두에게 접근 가능: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue
  2. 의미론적: getByAltText, getByTitle
  3. 테스트 ID (탈출구): getByTestId
// 최고
screen.getByRole("button", { name: /save/i });

// 입력에 적합
screen.getByLabelText("Email");

// 최후의 수단
screen.getByTestId("save-btn");

변형:

  • getBy* — 일치하는 항목이 없으면 예외를 던집니다.
  • queryBy* — null을 반환합니다 ("부재 어설션"에 사용).
  • findBy* — 비동기, Promise를 반환합니다 (비동기 작업 후 나타나는 요소에 사용).

userEvent를 사용한 사용자 상호 작용

import userEvent from "@testing-library/user-event";

test("submits the form", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
});
  • 항상 await을 사용하여 userEvent 호출을 기다리십시오.
  • 테스트당 한 번 userEvent.setup()을 호출하고 반환된 user를 재사용하십시오.
  • userEvent는 실제 브라우저 시퀀스를 시뮬레이션합니다. fireEvent는 단일 합성 이벤트를 전달합니다. userEvent를 선호하십시오.

비동기 패턴

// 비동기 작업 후 나타나는 요소
await expect(screen.findByText("Loaded")).resolves.toBeInTheDocument();

// 부수 효과 어설션
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// 사라져야 하는 요소
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

절대로 setTimeout + 어설션을 사용하지 마십시오. 불안정합니다. 위의 매처를 사용하십시오.

MSW를 사용한 네트워크 모킹

Mock Service Worker는 네트워크 레이어에서 모킹합니다. 컴포넌트, 훅, fetch 라이브러리는 모두 프로덕션에서와 정확히 동일하게 동작합니다.

설정

// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

export const server = setupServer(...handlers);

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

onUnhandledRequest: "error"를 구성하여 모킹되지 않은 요청이 테스트를 실패하게 만드십시오. 조용히 통과하는 것은 실패보다 더 나쁩니다.

테스트별 재정의

test("renders error on 500", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});

프로바이더 래핑

test-utils.tsx에서 프로바이더를 한 번 래핑하십시오:

// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions,
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={lightTheme}>
        <MemoryRouter>{ui}</MemoryRouter>
      </ThemeProvider>
    </QueryClientProvider>,
    options,
  );
}

export * from "@testing-library/react";

그런 다음 모든 테스트 파일에서 import { renderWithProviders, screen } from "test-utils"를 사용하십시오.

커스텀 훅 테스팅

import { renderHook, act } from "@testing-library/react";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter accepts initial value", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser fetches user data", async () => {
  // 래퍼 외부에서 테스트당 한 번 QueryClient를 인스턴스화하여 재렌더링 시에도 유지되도록 합니다.
  // 래퍼 클로저 내부에서 생성하면 모든 렌더링 시 캐시 상태가 재설정되어 불안정한 테스트가 발생합니다.
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
  • 상태 변경 호출을 act로 감싸십시오.
  • 훅의 퍼블릭 API를 통해서만 테스트하십시오.
  • 컨텍스트를 사용하는 훅의 경우 wrapper를 전달하십시오.

접근성 어설션

import { axe, toHaveNoViolations } from "jest-axe"; // 또는 vitest-axe
expect.extend(toHaveNoViolations);

test("UserCard has no a11y violations", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

모든 인터랙티브 컴포넌트에 대해 컴포넌트 테스트에서 axe를 실행하십시오. 다음을 발견합니다:

  • 폼 입력의 누락된 레이블
  • 잘못된 ARIA 사용
  • 낮은 색상 대비 (제한적 — JSDOM에는 실제 CSS 엔진이 없으므로 인라인 스타일에만 작동합니다. 시각적 대비는 Playwright에 속합니다.)
  • 이미지의 누락된 대체 텍스트
  • 제목 순서 위반

교차 참조: skills/accessibility/SKILL.md (광범위한 접근성 테스트 플레이북)

스냅샷 테스트를 사용하지 말아야 할 때

렌더링된 출력의 스냅샷:

  • 모든 스타일 변경 시 깨집니다.
  • 리뷰 중에 도장 찍히듯 통과됩니다.
  • 동작이 아닌 구현 세부 사항(DOM 구조)을 테스트합니다.

허용 가능한 스냅샷 사용:

  • 순수 데이터 직렬화 함수 (formatInvoice(invoice) -> 안정적인 문자열)
  • 생성된 설정 파일 (예: webpack 설정 출력)

컴포넌트의 시각적 회귀 테스트에는 Playwright/Cypress 스크린샷 또는 Percy/Chromatic을 사용하십시오. DOM 문자열이 아닌 실제 시각적 차이를 확인합니다.

Playwright / Cypress를 사용해야 할 때

JSDOM(Vitest/Jest에서 사용)은 다음을 할 수 없습니다:

  • 실제 레이아웃(flexbox, grid, 뷰포트 쿼리)을 렌더링합니다.
  • 네이티브 브라우저 애니메이션, CSS 전환을 실행합니다.
  • 스크롤 동작, 드래그 앤 드롭, 클립보드 붙여넣기를 테스트합니다.
  • iframe, 팝업, 다운로드, 교차 출처 흐름을 처리합니다.
  • 완전한 DevTools 지원과 함께 통제된 환경에서 실제 네트워크를 실행합니다.

이러한 경우 Playwright Component Testing (실제 브라우저에서 컴포넌트 테스트) 또는 전체 E2E를 사용하십시오. e2e-testing skill을 참조하십시오.

결정 경계:

  • 훅, 프리젠테이셔널 컴포넌트, 로직이 있는 폼 -> RTL
  • 레이아웃이 중요하거나 JSDOM에 없는 브라우저 API를 사용하는 컴포넌트 -> Playwright CT
  • 여러 페이지에 걸친 전체 사용자 흐름 -> Playwright/Cypress E2E

커버리지 목표

레이어목표
순수 유틸리티>=90%
커스텀 훅>=85%
프리젠테이셔널 컴포넌트>=80% — 라인이 아닌 동작 기준
컨테이너 컴포넌트>=70% — 골든 패스 + 오류 상태
페이지E2E는 별도로 커버; 최소 스모크 테스트

vitest.config.ts / jest.config.js를 통해 구성:

// vitest.config.ts
test: {
  coverage: {
    provider: "v8",
    reporter: ["text", "html", "lcov"],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
}

안티패턴

  • container.querySelector("...") — 접근성 쿼리를 우회하여 실제 사용자는 실패할 때 테스트가 통과하도록 합니다.
  • 렌더링 횟수 어설션 — 구현 세부 사항입니다.
  • jest.mock("react", ...) — 절대 React를 모킹하지 마십시오. 대신 컴포넌트를 리팩터링하십시오.
  • 기본적으로 자식 컴포넌트 모킹 — 통합이 아닌 격리를 테스트합니다. 자식에 무거운 부수 효과가 있을 때만 모킹하십시오.
  • act() 경고 무시 — 실제 버그(언마운트 후 상태 업데이트, 누락된 비동기 래핑)를 알리는 신호입니다.
  • 테스트 간에 가변 상태 공유 — 테스트 순서가 변경되면 불안정해집니다.
  • it.skip()을 제거해도 통과하는 테스트 — 테스트가 실제로 생각하는 대로 어설션하지 않습니다.

TDD 워크플로우

RED     -> 다음 요구 사항에 대한 실패하는 테스트 작성
GREEN   -> 통과하는 최소한의 컴포넌트 코드 작성
REFACTOR -> 컴포넌트 개선, 테스트는 계속 통과
REPEAT  -> 다음 요구 사항

새 컴포넌트의 경우:

  1. 컴포넌트의 prop 타입과 시그니처를 정의합니다.
  2. 가장 간단한 경우에 대한 첫 번째 테스트를 작성합니다.
  3. 올바른 이유로 실패하는지 확인합니다.
  4. 통과할 수 있을 만큼만 구현합니다.
  5. 다음 테스트 케이스를 추가합니다.
  6. 세 번째 유사한 테스트가 패턴을 드러낼 때 리팩터링합니다.

테스트 명령어

# Vitest
vitest                            # watch 모드
vitest run                        # 1회 실행
vitest run --coverage             # 커버리지 포함
vitest run path/to/file.test.tsx  # 단일 파일

# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx

# CI 모드
CI=true vitest run --coverage

관련

예제

MSW와 userEvent를 사용한 폼 제출

test("submits user form and shows success", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});

오류 경계 테스트

function Broken() {
  throw new Error("boom");
}

test("error boundary renders fallback", () => {
  // 예상되는 throw에 대해 React의 console.error 노이즈를 억제한 후 복원합니다.
  // 그래야 스파이가 테스트 간에 누출되어 다른 곳의 실제 오류를 숨기지 않습니다.
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("Something went wrong")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

Suspense 경계 테스트

test("shows loading then content", async () => {
  renderWithProviders(
    <Suspense fallback={<div>Loading...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("Loading...")).toBeInTheDocument();
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬