The useful rule is the one Testing Library 221,825 was built around: test the contract, not the construction. A component's contract is what it promises the person looking at the screen and the parent that renders it. Everything else is construction: which hook holds which value, how often the body ran.
| Contract | Assert with | Example |
|---|---|---|
| What the user sees | a query for text or role | the alert appears after a failed save |
| What the user can do | user-event plus a query | submit enables once both fields are filled |
| What the parent gets | a mock function | onSelect is called with the row id |
| What the outside world gets | a request handler | a PATCH is sent with the edited fields |
Renaming a state variable or replacing useState with useReducer changes nothing a user can observe, so it must not change a test.
The classic pyramid over-weights unit tests for front-end work, because an isolated React 7,897 component rarely has interesting logic in it. React bugs live in the seams, so the shape that fits is heaviest in the middle: static checking first (TypeScript and Linting Hooks), unit tests for reducers and custom hooks, integration tests for whole features — a form with its validation and submit handler is one test, not seven — and Playwright 26,280 for the few flows that must never break.
Write each test at the highest level that still fails for a single reason: if a broken form can only be diagnosed from a stack trace the test was too high, and if renaming a label breaks nine tests it was too low. Skip third-party code — React Router 139,386 tests its own Link — and styling that carries no meaning: an assertion on class="btn btn-primary" fails on a design refresh while a genuinely broken button passes.