Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
affaan-m avatar

React Testing

  • 1.5k installs
  • 238k repo stars
  • Updated August 5, 2026
  • affaan-m/everything-claude-code

react-testing is a Claude Code skill that helps developers write reliable, user-centric tests for React components, hooks, and pages using React Testing Library, Vitest, MSW, and axe.

About

react-testing is an Everything Claude Code skill for behavior-focused React tests using React Testing Library with Vitest or Jest, MSW for network mocking, and axe for accessibility assertions. The skill explains when to write component tests versus Playwright or Cypress end-to-end runs, how to test custom hooks, and how to migrate away from legacy Enzyme patterns. Developers reach for react-testing while adding coverage to untested components, fixing flaky UI tests, or asserting accessible behavior during feature work. It emphasizes user-visible behavior over implementation details and documents practical mocking boundaries for modern React codebases.

  • Behavior-focused tests using React Testing Library and userEvent
  • Network mocking with MSW and accessibility checks with axe
  • Custom hook testing patterns and Vitest/Jest setup guidance
  • Clear decision boundary between component tests and Playwright/Cypress E2E
  • Migration help from Enzyme or class-component testing patterns

React Testing by the numbers

  • 1,514 all-time installs (skills.sh)
  • +257 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #475 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/affaan-m/everything-claude-code --skill react-testing

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1.5k
repo stars238k
Last updatedAugust 5, 2026
Repositoryaffaan-m/everything-claude-code

How do you test React components with RTL and MSW?

Write reliable, user-centric tests for React components, hooks, and pages using React Testing Library, Vitest, MSW, and axe.

Who is it for?

Frontend developers adding or fixing tests for React components, hooks, or pages who want RTL, Vitest, MSW, and axe patterns in Claude Code.

Skip if: Teams needing only Playwright or Cypress end-to-end suites without component-level RTL coverage should not start with react-testing alone.

When should I use this skill?

The user writes or fixes tests for React components, hooks, or pages, migrates from Enzyme, or asks about MSW, axe, or Vitest testing patterns.

What you get

React component and hook test files, MSW handlers, accessibility assertions, and documented unit-versus-E2E test boundaries.

  • Component and hook test files
  • MSW mock handlers
  • Accessibility assertion examples

Files

SKILL.mdMarkdownGitHub ↗

React Testing

Comprehensive React testing patterns for behavior-focused component tests, custom hook tests, accessibility assertions, and network-level mocking.

When to Activate

  • Writing tests for React components, custom hooks, or pages
  • Adding test coverage to legacy untested components
  • Migrating from Enzyme or class-component-era patterns to React Testing Library
  • Setting up Vitest or Jest for a new React project
  • Mocking HTTP requests in tests
  • Asserting accessibility violations
  • Deciding which tests belong in RTL vs Playwright Component Testing vs full E2E

Core Principle

Test what the user sees and does, not implementation details.

A test should:

  • Render the component with the same providers it has in production
  • Interact with it via accessible queries (role, label) and userEvent
  • Assert visible output and observable side effects (callback fired, request sent)

A test should NOT:

  • Inspect component state, props passed to children, or which hooks were called
  • Mock React itself or framework hooks
  • Assert on the number of renders or DOM structure beyond what affects users

Library Choice

RunnerWhenNote
VitestVite, Remix, modern setupsFaster, native ESM, Jest-compatible API
JestNext.js, CRA, established reposDefault for many React projects
Playwright Component TestingReal browser engine neededUse when JSDOM lacks the required feature
Cypress Component TestingReal browser, Cypress already in useAlternative to Playwright CT

Pick one. Do not run RTL + Vitest AND Playwright CT in the same repo unless you have a clear lane separation.

Query Priority

React Testing Library exposes queries in three tiers — use top-down:

1. Accessible to everyone: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue 2. Semantic: getByAltText, getByTitle 3. Test IDs (escape hatch): getByTestId

// Best
screen.getByRole("button", { name: /save/i });

// OK for inputs
screen.getByLabelText("Email");

// Last resort
screen.getByTestId("save-btn");

Variants:

  • getBy* — throws if no match
  • queryBy* — returns null (use for "assert absence")
  • findBy* — async, returns a Promise (use for elements that appear after async work)

User Interaction with 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"), "user@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
});
  • Always await userEvent calls
  • Call userEvent.setup() once per test, reuse the returned user
  • userEvent simulates a real browser sequence; fireEvent dispatches a single synthetic event — prefer userEvent

Async Patterns

// Element that appears after async work
expect(await screen.findByText("Loaded")).toBeInTheDocument();

// Side effect assertion
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// Element that should disappear
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

Never setTimeout + assertion — flaky. Use the matchers above.

Network Mocking with MSW

Mock Service Worker mocks at the network layer. The component, hooks, and fetch library all behave exactly as in production.

Setup

// 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());

Configure onUnhandledRequest: "error" so any unmocked request fails the test loudly — silent passes are worse than red.

Per-test override

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

Provider Wrapping

Wrap providers once in a 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";

Then import { renderWithProviders, screen } from "test-utils" in every test file.

Custom Hook Testing

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 () => {
  // Instantiate QueryClient ONCE per test outside the wrapper so it survives re-renders.
  // Creating it inside the wrapper closure resets cache state on every render, producing flaky tests.
  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" });
});
  • Wrap state-changing calls in act
  • Test through the hook's public API only
  • For hooks that use context, pass a wrapper

Accessibility Assertions

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

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

Run axe in component tests for every interactive component. Catches:

  • Missing labels on form inputs
  • Invalid ARIA usage
  • Poor color contrast (limited — JSDOM has no real CSS engine, so this works for inline styles only; visual contrast belongs in Playwright)
  • Missing alt text on images
  • Heading order violations

Cross-link: skills/accessibility/SKILL.md for the broader a11y testing playbook.

When NOT to Use Snapshot Tests

Snapshots of rendered output:

  • Break on every styling change
  • Get rubber-stamped during review
  • Test implementation detail (DOM structure), not behavior

Acceptable snapshot uses:

  • Pure data serialization functions (formatInvoice(invoice) -> stable string)
  • Generated config files (e.g., webpack config output)

For visual regression on components, use Playwright/Cypress screenshots or Percy/Chromatic — actual visual diffs, not DOM strings.

When to Reach for Playwright / Cypress

JSDOM (used by Vitest/Jest) cannot:

  • Render real layout (flexbox, grid, viewport queries)
  • Run native browser animation, CSS transitions
  • Test scrolling behavior, drag-and-drop, paste from clipboard
  • Handle iframes, popups, downloads, cross-origin flows
  • Run real network in a controlled environment with full DevTools support

For any of those, use Playwright Component Testing (component test in real browser) or full E2E. See e2e-testing skill.

Decision boundary:

  • A hook, a presentational component, a form with logic -> RTL
  • A component whose layout matters or that uses browser APIs not in JSDOM -> Playwright CT
  • A full user flow across multiple pages -> Playwright/Cypress E2E

Coverage Targets

LayerTarget
Pure utilities>=90%
Custom hooks>=85%
Presentational components>=80% — behavior, not lines
Container components>=70% — golden paths + error states
PagesE2E covered separately; smoke test minimum

Configure via 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,
    },
  },
}

Anti-Patterns

  • container.querySelector("...") — bypasses accessibility queries, lets tests pass when real users would fail
  • Asserting on number of renders — implementation detail
  • jest.mock("react", ...) — never mock React. Refactor the component instead
  • Mocking child components by default — tests the integration, not isolation. Mock only when the child has heavy side effects
  • Ignoring act() warnings — they signal real bugs (state update after unmount, missing async wrapping)
  • Sharing mutable state across tests — flakes when test order changes
  • Tests that pass with it.skip() removed — your test does not actually assert what you think

TDD Workflow

RED     -> Write failing test for the next requirement
GREEN   -> Write minimal component code to pass
REFACTOR -> Improve the component, tests stay green
REPEAT  -> Next requirement

For new components:

1. Define the component's prop type and signature 2. Write the first test for the simplest case 3. Verify it fails for the right reason 4. Implement just enough to pass 5. Add the next test case 6. Refactor when the third similar test reveals a pattern

Test Commands

# Vitest
vitest                            # watch
vitest run                        # one-shot
vitest run --coverage             # with coverage
vitest run path/to/file.test.tsx  # single file

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

# CI mode
CI=true vitest run --coverage

Related

  • Rules: rules/react/testing.md
  • Skills: react-patterns, accessibility, e2e-testing, tdd-workflow
  • Agents: react-reviewer (reviews test quality during code review), tdd-guide (enforces TDD process)
  • Commands: /react-test, /react-review

Examples

Form submission with MSW and 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"), "alice@example.com");
  await user.click(screen.getByRole("button", { name: /save/i }));

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

Testing an error boundary

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

test("error boundary renders fallback", () => {
  // Suppress React's console.error noise for the expected throw, then restore so
  // the spy does not leak across tests and hide real errors elsewhere.
  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();
  }
});

Testing a Suspense boundary

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

Related skills

FAQ

What libraries does react-testing use?

react-testing standardizes on React Testing Library for components and hooks, Vitest or Jest as the runner, MSW for network mocking, and axe for accessibility assertions, with guidance on when Playwright or Cypress E2E is appropriate.

Should react-testing replace end-to-end tests?

react-testing focuses on fast, behavior-driven component and hook tests. The skill documents a clear boundary: use RTL with MSW for UI logic, and reserve Playwright or Cypress for full user journeys across pages.

Testing & QAfrontendtesting

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.