How Do You Test React Components?

Advanced12 min interview
Skills tested:
Understanding the Jest + RTL architecture and why RTL queries by role and labelUsing getByRole, getByLabelText, and getByText as primary query methodsUsing userEvent for realistic interaction simulation over fireEventHandling async assertions with waitFor and findBy queriesWriting tests from the user perspective without touching component internals

Advertisement

🧩 Scenario

Testing is a common senior interview topic. Interviewers look for whether you understand the philosophy behind RTL (test behavior not implementation), know the query priority order, and can handle async interactions correctly.

Architecture Walkthrough

The Jest + RTL Architecture

Jest is a test runner: it discovers test files, executes them in a Node environment (using jsdom to simulate a browser), provides assertion utilities (expect, toBe, toBeInTheDocument), and reports results. It also handles mocking modules, timing functions, and spying on calls.

React Testing Library is a DOM querying utility. It renders a component into the jsdom environment via render() and provides query functions to find elements: getByRole, getByLabelText, getByText, findByText, and others. RTL's design principle is that tests should mirror how real users find and interact with UI — by visible text, accessible roles, and input labels, not by internal implementation details like component state, class names, or test IDs.

Query Priority

RTL provides several query types. The recommended order from most to least preferred is: getByRole (finds anything with an ARIA role: buttons, inputs, links, headings), getByLabelText (finds form controls by their associated label), getByText (finds elements by visible text), getByPlaceholderText, and getByTestId (last resort only). Querying by role and label validates accessibility as a side effect: if getByLabelText('Email') cannot find the input, the label is missing, which is also an accessibility bug.

getBy vs queryBy vs findBy

All queries have three variants. getBy throws immediately if the element is not found. queryBy returns null if not found (use it to assert absence). findBy returns a Promise that resolves when the element appears or rejects after a timeout (use it for elements that appear after async work). Mixing up the variants is the most common source of flaky async tests.


Key Code Explained

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

// Synchronous test: find elements by role and label, interact, assert
test('shows error when email is invalid', async () => {
  const user = userEvent.setup();  // userEvent v14: setup() returns typed instance
  render(<LoginForm onSubmit={jest.fn()} />);

  // getByLabelText finds the input whose label text is "Email"
  // Also validates that the label is correctly associated
  await user.type(screen.getByLabelText('Email'), 'not-an-email');

  // getByRole('button', { name }) matches by accessible name
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  // getByText asserts the error message is visible in the DOM
  expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});


// Async test: form submission triggers an API call, state updates after response
test('calls onSubmit with credentials when form is valid', async () => {
  const user = userEvent.setup();
  const mockSubmit = jest.fn().mockResolvedValueOnce({ ok: true });
  render(<LoginForm onSubmit={mockSubmit} />);

  await user.type(screen.getByLabelText('Email'), 'user@example.com');
  await user.type(screen.getByLabelText('Password'), 'secret123');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  // waitFor polls until the assertion passes or times out
  await waitFor(() => {
    expect(mockSubmit).toHaveBeenCalledWith({
      email: 'user@example.com',
      password: 'secret123',
    });
  });
});


// Testing a component with async data fetching
// Mock the module so no real network requests are made
jest.mock('./api', () => ({
  fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));

test('displays user name after fetch resolves', async () => {
  render(<UserProfile userId={1} />);

  // findBy is shorthand for waitFor + getBy
  // It retries until the element appears (after the async fetch)
  const name = await screen.findByText('Alice');
  expect(name).toBeInTheDocument();
});


// Assert an element is NOT present
test('does not show admin panel for regular users', () => {
  render(<Dashboard userRole="user" />);

  // queryBy returns null instead of throwing — correct for absence checks
  expect(screen.queryByRole('region', { name: /admin/i })).not.toBeInTheDocument();
});

The jest.fn() pattern for onSubmit is fundamental. It creates a mock function that records every call, so assertions like expect(mockSubmit).toHaveBeenCalledWith(...) can verify that the component called the prop with the right arguments without needing a real server.


Tradeoffs

Query methodWhat it findsValidates a11yRecommended for
getByRoleElements by ARIA role + nameYesButtons, inputs, headings, landmarks
getByLabelTextForm controls by label textYesAll form inputs
getByTextElements by visible text contentPartiallyParagraphs, spans, static text
getByTestIdElements by data-testid attributeNoLast resort for elements without accessible identifiers

What Interviewers Actually Check

  • Whether you use getByRole and getByLabelText as primary queries
  • Whether you know userEvent over fireEvent and can explain why
  • Whether you know findBy for async elements and queryBy for absence assertions
  • Whether you avoid testing implementation details (state values, prop names, class names)
  • Whether you know how to mock modules and assert on mock function calls

Follow-Up Questions

  1. How do you test a component that uses useEffect to fetch data, without making real network requests?
  2. What is MSW (Mock Service Worker) and how does it differ from jest.mock for testing API calls?
  3. How do you test a component wrapped in a Context Provider (e.g., AuthContext, ThemeProvider)?
  4. How do you test error boundary fallback rendering in React Testing Library?
  5. What is the difference between a unit test and an integration test in the context of React components?

Common Candidate Mistakes

  • Querying by data-testid everywhere as a convenience shortcut rather than by role or label
  • Using fireEvent.change to type into inputs instead of userEvent.type — misses focus events that validation logic depends on
  • Not awaiting userEvent calls in v14 (they are async) — tests pass before interactions complete
  • Writing assertions on component state (component.state.isLoading) rather than visible DOM changes
  • Forgetting to wrap act-triggering code (state updates after interactions) properly, then suppressing the warning instead of fixing the test structure

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain what Jest does versus what RTL does?
  • Can you write a test that renders, interacts via userEvent, and asserts on DOM output?
  • Can you explain the query hierarchy and when to use each variant (getBy vs findBy vs queryBy)?
  • Can you mock a function prop with jest.fn() and assert it was called with the right arguments?
  • Can you handle async assertions with waitFor or findBy and explain the difference?

Summary

Jest is the test runner and assertion library. React Testing Library is a DOM querying utility built on the principle that tests should mirror how users interact with the UI. Query elements by role and label first (which validates accessibility), fall back to text, and use data-testid only when nothing else works. Use userEvent for interactions because it simulates the full sequence of real browser events. Use getBy when an element must be present, queryBy to assert absence, and findBy (or waitFor) for elements that appear after async operations. Avoid testing implementation details: state values, class names, or internal method calls. Tests should pass when the visible behavior is correct and fail when it is not, regardless of how the component is implemented internally.

Frequently Asked Questions

Why use userEvent instead of fireEvent?

fireEvent dispatches a single synthetic event (e.g., click). userEvent simulates the full sequence of events that real user interaction triggers: pointerover, pointerenter, pointermove, pointerdown, mousedown, focus, pointerup, mouseup, click. For typing, userEvent fires keydown, keypress, input, and keyup for each character. This catches bugs that fireEvent misses because real browser behavior fires multiple events in sequence.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement