Kodokon kodokon.com

Quality: Testing Library, StrictMode, Profiler

Test behavior rather than implementation, leverage StrictMode's double invocations, and measure renders with Profiler.

10 min · 3 questions

Open this lesson in Kodokon

The philosophy of Testing Library fits in one sentence: test what the user perceives, never the implementation details. Concretely, you query the DOM through the accessibility tree: getByRole first (the queen of queries, which validates your ARIA roles along the way), then getByLabelText, getByText, and getByTestId only as a last resort. For interactions, prefer user-event over fireEvent: where fireEvent.change emits an isolated event, user.type reproduces the full sequence (focus, keydown, keypress, input, keyup), exactly like a real keyboard. The findBy* variants combine a query with an async wait: essential after a loading step.

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

test('adds a task to the list', async () => {
  const user = userEvent.setup();
  render(<TodoApp />);

  await user.type(
    screen.getByRole('textbox'),
    'Review the report'
  );
  await user.click(
    screen.getByRole('button', { name: /add/i })
  );

  expect(
    await screen.findByText('Review the report')
  ).toBeInTheDocument();
});
A test focused on behavior, not implementation.

StrictMode is a development tool with no effect whatsoever in production. In development, it calls your component functions, state initializers, and functions passed to setState twice, to flush out impure renders: if two executions give different results, you have a hidden side effect. Since React 18, it also runs each effect through the mount, unmount, remount cycle: an effect whose cleanup is not perfectly symmetrical (subscription never cancelled, request never aborted) betrays itself immediately. It is direct preparation for features where React unmounts then remounts a screen while preserving its state.

JSX
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/user', { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch(() => {});
  return () => controller.abort();
}, []);
A symmetrical effect survives StrictMode's double mounting.

To measure, React provides the <Profiler> component and its graphical counterpart in the DevTools. Its onRender callback receives, among other things, phase (mount, update, or nested-update), actualDuration (the time actually spent rendering the subtree for this commit), and baseDuration (an estimate of the time to render the entire subtree without any memoization). The gap between the two measures the effectiveness of your memo and useMemo: an actualDuration far below baseDuration means memoization is working for you.

JSX
import { Profiler } from 'react';

function App() {
  return (
    <Profiler
      id="Sidebar"
      onRender={(id, phase, actualDuration) => {
        console.log(id, phase, actualDuration);
      }}
    >
      <Sidebar />
    </Profiler>
  );
}
Measuring the real render cost of a subtree.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why does StrictMode mount, unmount, then remount each effect in development?
    • To simulate server network latency
    • To check that each effect's cleanup is symmetrical
    • To clear React's internal cache
    • To force update batching
  2. Which Testing Library query should you reach for first?
    • getByTestId, the most stable
    • getByClassName, the most precise
    • getByRole, aligned with the accessibility tree
    • querySelector, the most flexible
  3. What does baseDuration represent in the Profiler callback?
    • The time of the last commit only
    • The estimated render time of the subtree without any memoization
    • The time elapsed since the component mounted
    • The average duration of the last ten renders