Guides expert-level react testing patterns implementation: typescript and testing decision frameworks, production-ready patterns, and concrete templates for react testing patterns workflows.
Use when the user asks about react testing patterns, react testing patterns configuration, or typescript best practices for react projects.
Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Guides expert-level react testing patterns implementation: typescript and testing decision frameworks, production-ready patterns, and concrete templates for react testing patterns workflows.
Use when the user asks about react testing patterns, react testing patterns configuration, or typescript best practices for react projects.
Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.
The user asks how to test a specific React component pattern -- hooks, context providers, compound components, portals, or async data fetching
The user wants to choose between testing approaches: React Testing Library vs Enzyme vs Cypress component testing vs Vitest
The user is writing TypeScript-typed tests and needs guidance on typing mocks, spy functions, or render helpers
The user wants to add accessibility assertions to their test suite using jest-axe, @testing-library/jest-dom, or aria role queries
The user needs to test components that depend on routing (React Router), global state (Redux, Zustand, Jotai), or server state (React Query, SWR)
The user is setting up a test configuration from scratch -- jest.config.ts, vitest.config.ts, setupTests.ts, or MSW handlers
The user asks about testing coverage thresholds, CI integration, or test performance (suite taking over 60 seconds)
The user needs to test error boundaries, Suspense boundaries, or React 18 concurrent rendering behavior
The user wants patterns for testing custom hooks in isolation without rendering a full component tree
The user asks about snapshot testing -- when to use it, when to avoid it, and how to maintain snapshots properly
Do NOT use this skill when:
The user needs help with end-to-end testing of full user flows across multiple pages -- use a dedicated E2E skill covering Playwright or Cypress test authoring
The user is asking about backend API testing, Node.js unit testing, or Express middleware testing -- those have different tool sets
The user is asking about performance profiling or React DevTools -- that is a separate profiling skill
The user needs help with React component architecture or design patterns unrelated to testing -- check the React architecture skill
The user is building a non-React frontend (Vue, Svelte, Angular, Lit) -- testing patterns differ meaningfully enough to require a different skill
The user only needs to configure CI/CD pipelines generically -- the testing patterns here assume tests are already written
The user is asking about visual regression testing with tools like Chromatic or Percy -- those require snapshot diffing workflows not covered here
Process
1. Establish the Testing Stack and Configuration
Before writing a single test, confirm the foundational toolchain and configuration:
Choose the test runner: Vitest is the recommended default for Vite-based projects (React + Vite, Next.js with custom Vite config). Jest remains the default for Create React App and Next.js projects not using Vite. Vitest offers 2-5x faster cold starts due to native ESM and shared Vite transform pipeline.
Set up React Testing Library (RTL): Install @testing-library/react, @testing-library/user-event (v14+, which uses async pointer events by default), and @testing-library/jest-dom. RTL's render is the primary entry point for all component tests.
Configure jsdom: Both Jest and Vitest need a DOM environment. Set testEnvironment: 'jsdom' in jest.config.ts or environment: 'jsdom' in vitest.config.ts. Add setupFilesAfterFramework: ['./src/setupTests.ts'] to auto-import @testing-library/jest-dom matchers.
TypeScript configuration for tests: Ensure tsconfig.json includes test files if you want type checking in tests, OR create a tsconfig.test.json that extends the base config with "types": ["vitest/globals", "@testing-library/jest-dom"] to avoid polluting production types.
Install MSW for network mocking:msw v2 with @mswjs/data is the production standard for intercepting fetch and Axios calls in tests without modifying application code. Initialize handlers in src/mocks/handlers.ts and the server in src/mocks/server.ts.
Add jest-axe for accessibility testing:jest-axe wraps the axe-core engine and integrates with jest/vitest matchers via expect(results).toHaveNoViolations(). This catches WCAG 2.1 AA violations automatically.
2. Classify What You Are Testing and Apply the Right Pattern
React tests fall into four distinct categories. Identify the category before choosing the pattern:
Unit: Pure UI components -- Components with no side effects, no data fetching, no context dependencies. Test rendering output for each significant prop combination. Use screen.getByRole queries (not getByTestId) as the primary assertion mechanism. Verify accessible names, not implementation details like class names.
Integration: Components with dependencies -- Components that consume context, call hooks that fetch data, or coordinate child components. Wrap in real or mocked providers. Use MSW to intercept API calls rather than mocking fetch directly.
Hook isolation tests -- Custom hooks tested with renderHook from @testing-library/react. This is preferable to testing hooks only through the components that use them, which creates brittle coupling.
Accessibility tests -- Run axe on rendered output after all async operations have settled. These complement but do not replace manual screen reader testing.
Decision rule: if a test imports a component file and also imports a mock of a module the component depends on via vi.mock() or jest.mock(), it is an integration test regardless of size. Name it accordingly and expect it to be slower (200-800ms range is acceptable; over 2 seconds indicates a problem).
3. Write Queries Using the RTL Priority Hierarchy
The order in which you query the DOM determines how resilient tests are to UI refactoring. Follow this strict priority:
getByRole with { name: 'Submit' } -- matches by ARIA role and accessible name. This is the single most important query. Use it for buttons, inputs, headings, checkboxes, links, dialogs, and listbox items.
getByLabelText -- for form inputs associated with a <label>. Catches missing label associations, which are accessibility bugs.
getByPlaceholderText -- fallback for inputs without labels; use it only when you control the placeholder and label is genuinely absent by design.
getByText -- for non-interactive content. Use { exact: false } when text contains dynamic values.
getByDisplayValue -- for reading the current value of an input, select, or textarea.
getByAltText -- for images. Ensures alt attributes exist.
getByTitle -- rarely appropriate; tooltips and title attributes are accessibility anti-patterns in most cases.
getByTestId -- LAST RESORT. Reserve for cases where the element has no semantic meaning (e.g., a loading skeleton div). When you use data-testid, prefix it with the component name (data-testid="UserCard-avatar") to create a namespace.
Never query by CSS class name (.querySelector('.btn-primary')). Class names are implementation details and change during refactors without breaking functionality.
4. Type Mocks and Render Helpers Correctly in TypeScript
Untyped mocks are a source of silent errors. Apply these patterns:
Typing vi.fn() / jest.fn() spy arguments: Use vi.fn<Parameters<typeof originalFn>, ReturnType<typeof originalFn>>() or simply let TypeScript infer from the mock implementation. Do NOT cast with as jest.Mock -- this erases type safety.
Partial mocks with vi.spyOn:vi.spyOn(module, 'functionName').mockReturnValue(expectedValue) -- TypeScript will enforce that expectedValue matches the function's real return type.
Typing mocked modules: When using vi.mock('../api/users'), create a typed helper:
vi.mocked() preserves the original function signature while allowing mock method calls like .mockResolvedValue().
Custom render functions: Create a src/test-utils/render.tsx that wraps RTL's render with all providers the app uses (QueryClientProvider, Router, ThemeProvider, Redux store). Type the wrapper's options parameter to extend RTL's RenderOptions:
Import from test-utils everywhere instead of directly from @testing-library/react.
5. Handle Async Behavior and State Updates
Async behavior is the most common source of flaky tests. Apply these patterns rigorously:
Always use userEvent.setup() at the top of the test function, not userEvent directly. userEvent.setup() returns a bound instance that correctly simulates pointer events, keyboard events, and focus management in sequence. userEvent.click() from the top-level import is the legacy API and does not simulate the full event chain.
Await all user interactions:await user.click(button) -- every interaction method on the userEvent instance is async in v14+.
Use waitFor for assertions that depend on async state changes:
Set a custom timeout when testing slow operations: waitFor(() => {...}, { timeout: 5000 }). Default is 1000ms.
Use findBy queries for elements that appear asynchronously:screen.findByRole('alert') is equivalent to waitFor(() => screen.getByRole('alert')) and is more readable for single-element assertions.
Wrap React 18 state updates in act(): RTL's render, userEvent methods, and waitFor all call act() internally. You should only call act() manually when you are triggering state updates outside of RTL's control (e.g., directly invoking a callback prop from outside the component).
Avoid await new Promise(resolve => setTimeout(resolve, 0)) (timer hacks). Use vi.useFakeTimers() and advance time explicitly with vi.advanceTimersByTimeAsync(1000) when testing debounced or throttled behavior.
6. Mock External Dependencies Correctly
The correct mocking strategy depends on the dependency type:
HTTP requests -- always use MSW: Define handlers in src/mocks/handlers.ts using http.get('/api/users', () => HttpResponse.json([...])). Override specific handlers per test using server.use(http.get('/api/users', () => HttpResponse.json([], { status: 500 }))) to simulate error states. MSW intercepts at the network level, which means your actual fetch/axios configuration is exercised, not bypassed.
Browser APIs not in jsdom: Mock window.matchMedia, IntersectionObserver, ResizeObserver, and navigator.clipboard in setupTests.ts. Provide implementations that return sensible defaults.
Timers: Use vi.useFakeTimers() in beforeEach and vi.useRealTimers() in afterEach for tests involving setTimeout, setInterval, Date.now(), or debounce logic.
Modules that import environment variables: Mock at the module level using vi.mock('../config', () => ({ API_URL: 'http://localhost:3000' })). Never read process.env directly in component code -- centralize env access in a config module so it can be mocked cleanly.
React Router navigation: Use MemoryRouter with initialEntries prop for route-based tests. For useNavigate, spy on the navigate function using vi.spyOn rather than mocking the entire module.
Do NOT mock child components unless they are third-party with heavy setup (e.g., a map library that requires canvas). Mocking child components makes tests brittle and hides integration bugs. Test the full component tree.
7. Test Custom Hooks with renderHook
Custom hooks deserve isolated tests separate from the components that consume them:
Import renderHook and act from @testing-library/react. Wrap state-updating calls in act() when calling them outside of RTL's async utilities.
Wrap hooks that require providers using the wrapper option:
const { result } = renderHook(() =>useUserProfile(userId), {
wrapper: ({ children }) =><QueryClientProviderclient={queryClient}>{children}</QueryClientProvider>,
});
Access the hook's return value via result.current. After triggering updates, always re-read result.current -- the reference is live.
For hooks that return async state, use waitFor to poll until the expected state is reached:
Test hook error states by configuring MSW to return error responses. Do not mock the hook's internal fetch calls directly -- that defeats the purpose of testing the hook's error handling logic.
8. Enforce Accessibility and Coverage Standards
Accessibility testing should be automated and mandatory, not optional:
Use RTL's ARIA role queries as a first-pass accessibility check: If you cannot query an element by role and name, the element likely lacks proper ARIA semantics. Fix the component, not the test.
Set meaningful coverage thresholds: 80% line coverage is the industry minimum for production React applications. Set branch coverage to 75% -- this catches untested conditional rendering paths. Do not aim for 100% coverage on everything; focus coverage on business logic hooks and complex conditional rendering, not on simple presentational components.
Exclude from coverage: Add /* v8 ignore next */ or /* istanbul ignore next */ annotations sparingly and only for truly untestable code (e.g., environment guard clauses). Exclude generated files (GraphQL types, icon components) via coverage.exclude in your config.
Output Format
When helping a user implement or review React testing patterns, provide output in this structure:
## Test Strategy for [Component or Hook Name]
### Classification
- Test Category: [Unit | Integration | Hook | Accessibility]
- Dependencies: [List context providers, APIs, router, stores]
- Async Behavior: [Yes/No -- describe what is async]
### Setup
```typescript
// Required imports
import { render, screen, waitFor } from '../test-utils/render';
import userEvent from '@testing-library/user-event';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
// MSW handler overrides for this test file
MSW Handlers: [list endpoints and response shapes]
Module Mocks: [list vi.mock calls with rationale]
Browser API Stubs: [list any window/navigator mocks needed]
Coverage Targets
Lines: [target %]
Branches: [target %]
Key paths to cover: [list critical conditional branches]
---
## Rules
1. **NEVER use `getByTestId` as the primary query strategy.** Every `data-testid` in production component code is a missed opportunity to enforce accessibility semantics. Reserve `getByTestId` for skeleton loaders, animation containers, and other elements with no semantic role. If you add more than 2 `data-testid` attributes in a single component, the component lacks proper ARIA structure -- fix the component.
2. **NEVER import directly from `@testing-library/react` in test files.** Always import from the custom `test-utils/render` wrapper that includes all providers. Tests that forget to wrap in providers produce false positives (the component renders with undefined context and may not crash, but the test is not realistic).
3. **NEVER use `fireEvent` when `userEvent` is available.** `fireEvent.click()` dispatches a single click DOM event. `userEvent.click()` dispatches the full sequence: `pointerover`, `pointerenter`, `mouseover`, `mouseenter`, `pointermove`, `mousemove`, `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click`. Components with hover states, focus traps, or drag interactions will behave differently under `fireEvent` than in a real browser.
4. **NEVER use `waitFor` with multiple assertions.** A single `waitFor` callback should assert exactly one condition. Multiple assertions inside `waitFor` create timing issues where the first assertion passes but the second fails intermittently. Chain `waitFor` calls or use `findBy` for the first async element, then use synchronous `expect` for subsequent assertions on elements already in the DOM.
5. **NEVER mock modules that are under test.** If you are testing `useAuthFlow`, do not mock `useAuthFlow`'s internal calls to `useTokenRefresh` unless `useTokenRefresh` makes real network calls (in which case, use MSW to mock the network, not the hook). Mocking internals means you are testing the mock, not the code.
6. **ALWAYS reset MSW handlers in `afterEach`.** Call `server.resetHandlers()` after each test. Failing to do so causes handler overrides from one test to bleed into subsequent tests, causing intermittent failures that are extremely difficult to diagnose.
7. **NEVER use snapshot tests for components with dynamic content** (dates, IDs, random values, user-generated content). Snapshots will fail on every run. Use inline assertions instead. Reserve snapshots for purely static components (icon sets, static marketing blocks) and commit to updating them deliberately.
8. **ALWAYS test the error boundary and Suspense fallback states explicitly.** The most common untested code path in React applications is the error state. Write at least one test per data-fetching component that configures MSW to return a 500 error, then asserts the error UI renders correctly and the error message is accessible.
9. **NEVER place `vi.mock()` or `jest.mock()` calls inside `describe` or `it` blocks.** Mock calls are hoisted to the top of the file by the bundler at compile time regardless of where you write them. Placing them inside blocks creates code that reads as if scoping applies when it does not, producing confusing behavior.
10. **ALWAYS clean up side effects in `afterEach`.** Call `vi.clearAllMocks()` (resets call history but keeps implementations) or `vi.resetAllMocks()` (resets implementations too) depending on whether mock implementations are set in `beforeEach`. Use `vi.restoreAllMocks()` only when you used `vi.spyOn` and want to restore the original implementation. Inconsistent cleanup is the root cause of over 60% of intermittent test failures in React projects.
---
## Edge Cases
### Testing Components Wrapped in Multiple Nested Providers
When a component requires 4+ context providers (Router + QueryClient + Redux + Theme + I18n), the `AllProviders` wrapper in `test-utils/render.tsx` can become unwieldy and tests become slow due to provider initialization. Create a `createTestQueryClient()` factory that returns a `QueryClient` with `retry: false` and `gcTime: 0` -- this prevents React Query from retrying failed requests (which would cause tests to hang for several seconds) and from caching data between tests. Set `staleTime: 0` so that components always refetch when they mount in tests, giving you control through MSW handlers. Create the `QueryClient` fresh in each test's `beforeEach`, not once at module level, to prevent query cache pollution between tests.
### Testing Components That Use Portals
Components that render into `document.body` via `ReactDOM.createPortal` (modals, tooltips, toasts) appear outside the container returned by `render()`. Query them using `screen.*` queries (which search the entire document) rather than `within(container).*` queries. When testing modal focus management, use `document.activeElement` to assert focus has moved into the portal. To test that `Escape` closes the modal, use `await user.keyboard('{Escape}')` -- not `fireEvent.keyDown` -- to correctly simulate the full keyboard event sequence.
### Testing Components with IntersectionObserver (Infinite Scroll, Lazy Images)
jsdom does not implement `IntersectionObserver`. Create a mock in `setupTests.ts`:
```typescript
const mockIntersectionObserver = vi.fn().mockImplementation((callback) => ({
observe: vi.fn().mockImplementation(() => callback([{ isIntersecting: true }])),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
window.IntersectionObserver = mockIntersectionObserver;
This mock fires the callback immediately with isIntersecting: true, simulating an element entering the viewport. For tests that need to verify behavior when NOT in the viewport, create an alternate mock that passes isIntersecting: false.
Testing Error Boundaries
React error boundaries require a component to throw during rendering. In tests, create a ThrowOnRender test component:
Suppress the expected console.error output that React prints when an error boundary catches an error -- otherwise it pollutes test output and can cause false CI failures. Use vi.spyOn(console, 'error').mockImplementation(() => {}) in beforeEach and restore it in afterEach. Assert that the error boundary's fallback UI renders and is accessible using axe.
Testing Components with useEffect Data Fetching Without React Query
Components that fetch in useEffect directly (using fetch or axios) require careful async handling because the fetch fires after render. Configure the MSW handler before rendering. Use await screen.findByRole(...) to wait for the loaded state rather than waitFor(() => screen.getByRole(...)) -- findBy has cleaner semantics for single-element async appearance. Always test the loading state by making the MSW handler delay its response: http.get('/api/data', async () => { await delay(100); return HttpResponse.json(data); }). Use vi.useFakeTimers() to control that delay precisely.
Testing Zustand or Jotai Stores in Isolation
Global stores that persist state across tests cause the most insidious test pollution. For Zustand, create a store with createStore (not create) so you can instantiate a fresh store per test:
Pass the test store into the component wrapper using the store's context provider. For Jotai, wrap each test's render in a <Provider store={createStore()}> where the store is created fresh. Never export a global singleton store from your store module -- always export the creator function.
Testing React 18 Concurrent Features (Transitions and Deferred Values)
useTransition and useDeferredValue use React's scheduler and do not flush synchronously in tests. When testing components that use startTransition, wrap the interaction that triggers the transition in act() explicitly, then use waitFor to assert the post-transition state. Be aware that isPending state from useTransition may be true for only a single render frame -- test it by intercepting a slow MSW response and asserting the pending indicator appears before the response arrives. Set the MSW handler delay to 50ms and assert isPending state within a waitFor with a timeout: 100.
Testing Components in a Monorepo with Shared Packages
When a tested component imports from a shared internal package (@company/design-system, @company/auth), configure the test runner's module resolver to use the TypeScript source of those packages rather than their compiled output. Add path aliases in vitest.config.ts under resolve.alias:
This eliminates the need to rebuild shared packages before running tests, cutting CI time by 30-60% in monorepo setups. If shared packages have their own setupTests.ts, import them in the consuming package's setup file to avoid duplicating browser API mocks.
Example
Input: "I have a UserProfileCard component in TypeScript that fetches user data with React Query's useQuery, shows a loading skeleton, shows an error message on failure, and has a 'Follow' button that calls a POST endpoint. How do I test it properly?"
Output:
Test Strategy for UserProfileCard
Classification
Test Category: Integration (React Query + network calls + user interaction)
Dependencies: QueryClientProvider, React Query's useQuery and useMutation, MSW for network interception
Async Behavior: Yes -- initial data fetch on mount, mutation on button click
Setup
First, ensure MSW handlers cover both endpoints. In src/mocks/handlers.ts: