Verdict
Yes
Yes, give each UI component that has behaviour one unit test of its main interaction, written with Testing Library against what the user sees, and skip components that only lay out their props.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilitysame-day
- Reversibilitytrivial
- Test costmoderate
Test minimally: give each component with behaviour one unit test of its main interaction. For a React or Vue component in a product web app, Blast radius is users and Change frequency is regularly, because product teams change a component about once a month. Detectability is same-day, because customers report a dead control within a day, and Reversibility is trivial, because a revert leaves nothing behind. Test cost is moderate: a Testing Library test takes about an hour once the component needs a router or a store, and it changes with the behaviour. Rule R12 gives Test minimally; with Test cost heavy, the decision is Do not test.
| When | Decision | Why |
|---|---|---|
| The component only lays out its props, such as a user card, on one screen that its author opens after each change | Do not test it; let TypeScript check the props and look at the screen before merging | Detectability moves to immediately, because the author sees the mistake at once, and Reversibility stays trivial |
| The component assembles the data the app saves, such as a multi-step profile form | Test: assert the submitted payload for the main path and for each optional step | Detectability moves to eventually and Reversibility rises to with-effort, because a dropped field shows nothing on screen and saved records need a repair script |
| The component shows a price, a discount or an order total | Test mandatory: assert the exact amount shown for each pricing case the component handles | Blast radius rises to money and Reversibility to costly, because customers buy at the amount on the screen |
| The failure to catch is visual, such as a dialog whose buttons fall off small screens | Do not unit test the layout; check the component in a browser at each screen width you support | Test cost rises to heavy, because jsdom computes no layout and a unit test cannot see the overflow |
| A shared design-system component, such as a date picker, appears on dozens of screens | Test minimally: one interaction test of its default variant | Change frequency falls to rarely and Detectability moves to eventually, because a break shows on screens its author never opened, so the decision holds |
| The component ships in a React Native app without over-the-air updates | Test: cover the main interaction and the empty and error states | Reversibility rises to costly, because every fix waits for an app store release |
What breaks if you don't test
A refactor breaks an interaction that nobody clicks before merging. A checkbox loses its onChange handler, the page renders exactly as before, and the type checker passes. The first person to click is a customer, who reports the dead checkbox the same day, and until the revert ships nobody can mark a task done on that screen.
What you lose if you over-test
A test that reads internal state or counts renders fails on every refactor that keeps the behaviour: Enzyme's state() has nothing to read once a class component moves to hooks. A test for each prop combination of a layout component repeats what TypeScript already checks, and every markup change breaks several of them at once. A render-only test stays green while every button is dead, and the coverage report counts the component as tested.
How to test
Test at the unit level with Testing Library in Jest or Vitest, one test per component that has behaviour:
- Render the component with realistic props and the providers it needs.
- Perform the main interaction with user-event, which fires the full sequence of events that a real click causes.
- Assert what the user sees, or what the component reports to its parent, through a role query.
Skip layout-only components and internal state. Add a regression test when a bug reaches production.
When the answer changes
- The component builds data that the app saves.
- The component shows a price or a total.
- The bugs that reach production are visual, such as clipped dialogs on phones.
Real incident + Code example
The checkbox that stopped listening
On a project management app I worked on, a design-system cleanup replaced the native checkbox in each task row with a shared Checkbox component. The row passed checked but not onChange, which was optional, so TypeScript accepted it and React only logged a console warning. Nobody clicked a checkbox in review. Customers reported within three hours that tasks could not be marked done, and the revert took twenty minutes. We added this test to every row with a control:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test, vi } from 'vitest';
import { TaskRow } from './TaskRow';
test('reports the task as done when its checkbox is clicked', async () => {
const user = userEvent.setup();
const onToggle = vi.fn();
render(
<TaskRow
task={{ id: 't1', title: 'Draft the agenda', done: false }}
onToggle={onToggle}
/>,
);
await user.click(screen.getByRole('checkbox', { name: 'Draft the agenda' }));
expect(onToggle).toHaveBeenCalledWith('t1', true);
});
The test ignores markup, so a correct switch to the shared Checkbox passes it and the lost handler fails it.
Related questions
FAQ
- Should you unit test React components?
Yes, unit test a React component that has behaviour, with one Testing Library test of its main interaction. A component that only renders its props needs no test, because TypeScript checks the props.
- Should I bother to write unit tests for UI components?
Write unit tests for UI components that react to input, such as forms, toggles and filters, and skip those that only display data. One interaction test takes about an hour and catches a dead control before customers do.
- Should you test that all components render without crashing?
No, a render-without-crashing test catches only an exception during the first render, and a dead button passes it. Write one interaction test per component with behaviour instead, which also fails on a crash.