Loading...
Loading...
React testing best practices using React Testing Library, Vitest, and Jest. Use when writing, reviewing, or generating tests for React components, hooks, context providers, async interactions, or form submissions. Triggers on tasks like "write a test for this component", "add unit tests", "test this hook", "mock this API call", "improve test coverage", or "set up Vitest".
npx skill4agent add rutpshah/skills-react-testing-best-practices react-testing-best-practicesnpm 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";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"],
};getByRolegetByLabelTextgetByPlaceholderTextgetByTextgetByTestIddata-testidquerySelectorgetElementsByClassName.find('.classname')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();
});
});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",
});
});waitForfindBy*fetchaxiosimport { 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— it intercepts at the network level, making tests more realistic.vi.mock('axios')
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);
});const wrapper = ({ children }) => <ThemeProvider>{children}</ThemeProvider>;
const { result } = renderHook(() => useTheme(), { wrapper });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();
});renderWithProviderssrc/test/utils.tsx// src/test/utils.tsx
export * from "@testing-library/react";
export { renderWithProviders as render };// 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(() => {});afterEach(() => vi.restoreAllMocks())npm install -D jest-axeimport { 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();
});| ❌ Avoid | ✅ Do instead |
|---|---|
| |
| Query the DOM output directly |
| |
| |
| Asserting internal state | Assert visible UI changes |
Empty | Group only related tests; flat is fine |
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 setupnpm installvitestgithub.com/rutpshah/skillsreferences/testing-patterns.md