react-testing-best-practices
npx skills add https://github.com/rutpshah/skills --skill react-testing-best-practices
Agent 安装分布
Skill 文档
React Testing Best Practices
Comprehensive testing patterns for React applications using React Testing Library (RTL), Vitest, and Jest.
Core Philosophy
Test behavior, not implementation. Users interact with the DOM â tests should too.
- Query by what users see: roles, labels, text â not class names or internal state
- Avoid testing implementation details (state variables, internal methods)
- Prefer integration-level tests over isolated unit tests for components
- One assertion focus per test; use descriptive test names
Setup
Vitest + RTL (recommended for Vite projects)
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
// vite.config.ts
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.ts",
},
});
// src/test/setup.ts
import "@testing-library/jest-dom";
Jest + RTL (for Create React App / Next.js)
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
// jest.config.js
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterFramework: ["@testing-library/jest-dom"],
};
Query Priority (RTL)
Always prefer in this order:
getByRoleâ most accessible, mirrors how screen readers see the pagegetByLabelTextâ for form fieldsgetByPlaceholderTextâ fallback for inputsgetByTextâ for non-interactive contentgetByTestIdâ last resort only; usedata-testidsparingly
â Never use: querySelector, getElementsByClassName, enzyme’s .find('.classname')
Component Testing
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
describe("Button", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Submit</Button>);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(handleClick).toHaveBeenCalledOnce();
});
it("is disabled when loading", () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
});
Form Testing
it("submits the form with user input", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), "user@example.com");
await user.type(screen.getByLabelText(/password/i), "secret123");
await user.click(screen.getByRole("button", { name: /log in/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: "user@example.com",
password: "secret123",
});
});
Async & API Testing
Use waitFor or findBy* for async state changes. Always mock fetch or axios at the module level.
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
http.get("/api/users", () => {
return HttpResponse.json([{ id: 1, name: "Alice" }]);
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it("renders users from API", async () => {
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const user = await screen.findByText("Alice");
expect(user).toBeInTheDocument();
});
it("shows error on API failure", async () => {
server.use(http.get("/api/users", () => HttpResponse.error()));
render(<UserList />);
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});
Prefer MSW (Mock Service Worker) over
vi.mock('axios')â it intercepts at the network level, making tests more realistic.
Custom Hook Testing
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
it("increments the counter", () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
For hooks that depend on context, wrap with a provider:
const wrapper = ({ children }) => <ThemeProvider>{children}</ThemeProvider>;
const { result } = renderHook(() => useTheme(), { wrapper });
Context & Provider Testing
const renderWithProviders = (ui, options = {}) => {
const { store = setupStore(), ...renderOptions } = options;
const Wrapper = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme="light">{children}</ThemeProvider>
</Provider>
);
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
};
// Usage
it("shows user name from store", () => {
const store = setupStore({ user: { name: "Alice" } });
renderWithProviders(<Header />, { store });
expect(screen.getByText("Alice")).toBeInTheDocument();
});
Extract renderWithProviders into src/test/utils.tsx and re-export from RTL:
// src/test/utils.tsx
export * from "@testing-library/react";
export { renderWithProviders as render };
Mocking
// Mock a module
vi.mock("../utils/api", () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));
// Mock only part of a module
vi.mock("../utils/date", async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, formatDate: vi.fn(() => "Jan 1, 2025") };
});
// Spy on a method
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
Always restore mocks: afterEach(() => vi.restoreAllMocks())
Accessibility Testing
npm install -D jest-axe
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
it("has no accessibility violations", async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Common Mistakes to Avoid
| â Avoid | â Do instead |
|---|---|
getByTestId('submit-btn') |
getByRole('button', { name: /submit/i }) |
wrapper.find(MyComponent) |
Query the DOM output directly |
act() around every interaction |
userEvent handles act() internally |
fireEvent.click() |
await userEvent.click() â more realistic |
| Asserting internal state | Assert visible UI changes |
Empty describe blocks |
Group only related tests; flat is fine |
File Naming & Organization
src/
components/
Button/
Button.tsx
Button.test.tsx â colocate tests
hooks/
useCounter.ts
useCounter.test.ts
test/
setup.ts â global setup
utils.tsx â renderWithProviders, custom matchers
mocks/
handlers.ts â MSW handlers
server.ts â MSW server setup
Additional Patterns
See references/testing-patterns.md for:
- Snapshot testing guidance
- Testing React Router navigation
- Testing with React Query / TanStack Query
- Testing drag-and-drop interactions
- Visual regression testing with Playwright