react-testing
React Testing Techniques
Best practices for testing React components with Vitest, React Testing Library (RTL), and Mock Service Worker (MSW).
Core Principles
Test Like a User
Write tests that interact with components the way users do. Avoid testing implementation details.
- Query elements by accessible roles, labels, and text - not by test IDs or class names
- Simulate real user interactions with
userEvent, notfireEvent - Test behavior and outcomes, not internal state or method calls
- Prefer integration tests over isolated unit tests
Prefer Integration Over Mocking
Combine multiple components and use MSW for API mocking at the network level. Avoid mocking component internals, hooks, or services unless absolutely necessary.
Query Selection Decision
Choose the right query based on the scenario:
| Scenario | Query | Why |
|---|---|---|
| Element exists synchronously | getBy* |
Throws if not found - fails fast |
| Element appears after async operation | findBy* |
Waits with retry, use with await |
| Assert element does NOT exist | queryBy* |
Returns null instead of throwing |
| Multiple elements expected | *AllBy* variants |
Returns array of matches |
Priority order for queries:
getByRole- most accessible, preferredgetByLabelText- for form fieldsgetByPlaceholderText- for inputsgetByText- for non-interactive contentgetByTestId- last resort only
UserEvent Setup (Required Pattern)
Always use userEvent.setup() before interactions:
import { render, screen } from '../test/test-utils';
import userEvent from '@testing-library/user-event';
test('submits form correctly', async () => {
const user = userEvent.setup();
render(<MyForm />);
await user.type(screen.getByRole('textbox', { name: /email/i }), 'test@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(await screen.findByText(/success/i)).toBeInTheDocument();
});
Never use fireEvent - it dispatches raw DOM events without realistic user interaction simulation.
Async Testing Patterns
Use findBy* for Elements That Appear Async
// Element appears after data loads
expect(await screen.findByText('John Doe')).toBeInTheDocument();
Use waitFor for Assertions Only
// Wait for a side effect or state change
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
Use waitForElementToBeRemoved for Disappearing Elements
// Wait for loading spinner to disappear
await waitForElementToBeRemoved(() => screen.getByText('Loading...'));
MSW v2 Patterns
Use Mock Service Worker for API mocking. Always use v2 syntax:
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'John' }]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Override handlers per test:
test('handles error state', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
})
);
// Test error handling...
});
Vitest-Specific Patterns
Use Vitest APIs, not Jest:
| Operation | Vitest | Not Jest |
|---|---|---|
| Mock function | vi.fn() |
|
| Mock module | vi.mock() |
|
| Spy on method | vi.spyOn() |
|
| Stub env var | vi.stubEnv() |
|
| Reset modules | vi.resetModules() |
|
| Restore mocks | vi.restoreAllMocks() |
Environment variable testing:
import { afterEach, vi } from 'vitest';
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});
test('handles missing env var', async () => {
vi.stubEnv('API_KEY', '');
const { config } = await import('./config');
expect(() => config.apiKey).toThrow();
});
Test Utils Setup
Create a custom render with all providers. Read references/test-utils-setup.md for complete setup with:
- MantineProvider (with
env="test") - QueryClientProvider (fresh client per test)
- RouterProvider (TanStack Router)
What to Avoid
Never Test Implementation Details
- Internal state
- Private methods
- Component lifecycle
- Hook internals
Never Use Snapshot Tests
Except for trivial components (<30 lines of output). Snapshots encode structure, not intent.
Never Mock What You Don't Own
Use MSW for APIs instead of mocking fetch/axios. Mock only at system boundaries.
Additional Resources
Reference Files
Consult these for detailed patterns:
references/test-utils-setup.md- Complete custom render setup with providersreferences/query-selection.md- Detailed query type decision guidereferences/vitest-patterns.md- Vitest mocking, spying, and stubbingreferences/msw-patterns.md- MSW v2 handlers, overrides, error simulationreferences/async-testing.md- waitFor, findBy, timing patternsreferences/tanstack-testing.md- TanStack Query and Router testingreferences/user-interactions.md- userEvent patterns and edge casesreferences/mantine-testing.md- Mantine component testing (modals, forms, select)
More from mrclrchtr/skills
agent-orchestrator
Orchestrate complex work via a phase-gated multi-agent loop (audit → design → implement → review → validate → deliver). Use when you need to split work into subsystems, run independent audits, reconcile findings into a confirmed issue list, delegate fixes in clusters, enforce PASS/FAIL review gates, and drive an end-to-end validated delivery. Do not use for small, single-file tasks.
38git-commit
Creates a commit: detects conventions, stages intentionally, writes a clear subject, add a concise body when useful, and commits.
22agent-orchestrator-standalone
Orchestrate complex work via a phase-gated multi-agent loop (audit → design → implement → review → validate → deliver). Use when you need to split work into subsystems, run independent audits, reconcile findings into a confirmed issue list, delegate fixes in clusters, enforce PASS/FAIL review gates, and drive an end-to-end validated delivery. Do not use for small, single-file tasks.
16commit
Creates a commit with repo-matching style and intentional staging.
4web-fetch-to-markdown
Fetches and normalizes http(s) web pages into clean Markdown for LLM ingestion. Use when a task includes a URL, needs to fetch docs or asks to convert web docs/articles/pages into Markdown for summarizing, quoting, diffing, or saving.
4web-design-guidelines-design
Use when creating, redesigning, or restyling a UI and Codex should establish a clear design direction before implementation.
4