← Back to Jest Mastery
Intermediate20 min read

React Testing with Jest

Test React components with Jest and React Testing Library using user-centric queries.

Jest with React Testing Library

Use React Testing Library (RTL) instead of Enzyme for modern React testing. RTL encourages querying by accessible roles and labels—the way users find elements—not by component instance internals.

Configure testEnvironment: "jsdom" and import @testing-library/jest-dom in setupFilesAfterEnv for DOM matchers like toBeInTheDocument.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

test("submits credentials", async () => {
  const user = userEvent.setup();
  const onSubmit = jest.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText(/email/i), "ada@example.com");
  await user.type(screen.getByLabelText(/password/i), "secret");
  await user.click(screen.getByRole("button", { name: /sign in/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "ada@example.com",
    password: "secret",
  });
});

Rendering Components

render mounts component into jsdom document. Query with screen.getByRole, getByLabelText, getByText. Use queryBy variants when element may not exist.

Wrap components needing context with custom render helpers supplying Router, Redux Provider, or QueryClientProvider.

  • Avoid enzyme shallow rendering—it tests implementation details
  • Use data-testid only when roles and labels are insufficient
  • Cleanup is automatic with RTL render in modern versions
function renderWithRouter(ui: React.ReactElement) {
  return render(<MemoryRouter>{ui}</MemoryRouter>);
}

Testing Props and State Behavior

Assert on visible outcomes when props change—re-render with rerender from RTL. User interactions through userEvent trigger state updates; await async results with findBy queries or waitFor.

Do not test state directly or call setState on instances—test what users see and do.

const { rerender } = render(<Badge count={1} />);
expect(screen.getByText("1")).toBeInTheDocument();

rerender(<Badge count={99} />);
expect(screen.getByText("99")).toBeInTheDocument();

Integration-Style Component Tests

Test components together with real child components when integration adds confidence. Mock only network with MSW, not child UI components.

MSW handlers return fixture data; assert loading, success, and error UI states with async queries.

import { rest } from "msw";
import { setupServer } from "msw/node";

const server = setupServer(
  rest.get("/api/user", (_req, res, ctx) => res(ctx.json({ name: "Ada" })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Common Pitfalls

Wrapping everything in act manually is rarely needed with RTL and userEvent—they handle act internally. Missing await on userEvent causes act warnings and flakes.

Testing implementation details (component state names, hook call order) breaks on refactors without catching user-visible bugs.

  • Prefer getByRole with accessible name over CSS class selectors
  • Use findBy for elements appearing after fetch
  • Keep component tests fast—mock heavy dependencies at network boundary

Get In Touch


Ready to discuss your next project? Drop me a message.