After clicking "Add to cart" in a React Testing Library test, a developer asserts expect(useCartStore.getState().items).toHaveLength(1) on the Zustand store instead of asserting on what the page renders. What is wrong with that assertion, and when is asserting on state acceptable?
answer
- stops one step short
- state changed, screen did not
- one assertion implies the other
- the store has its own test
- seeding is fine, asserting is not
basics
~20 sAsserting on store state proves the plumbing ran, not that the user saw anything: the store can update while the component fails to render the change. Assert the rendered outcome instead, and the state assertion becomes redundant.
solid answer
~50 sThe store assertion stops one step short of the thing being tested. It passes when the click handler dispatched correctly and the component never re-rendered, never subscribed to the right slice, or rendered the count in a branch that is currently unreachable — every failure between the state change and the pixels is invisible to it. The component's job is to turn state into something a user can perceive, so the assertion should be on that: `expect(await screen.findByText('1 item in cart')).toBeInTheDocument()`. If the DOM shows the right thing, the state must have been right, so the DOM assertion strictly dominates. State is a legitimate subject in the store's own unit test, where the store — not the component — is what is under test. Inside a component test it is a shortcut that trades away the only coverage the test was there to provide.
code
jsx · 16 linesimport { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useCartStore } from './cartStore';
import Cart from './Cart';
test('adding an item updates the cart', async () => {
const user = userEvent.setup();
render(<Cart />);
await user.click(screen.getByRole('button', { name: /add to cart/i }));
// passes even if <Cart> never re-renders the new item
expect(useCartStore.getState().items).toHaveLength(1);
// fails whenever the user would not see the result
expect(await screen.findByText('1 item in cart')).toBeInTheDocument();
});go deeper
Be ready to say what a component test is checking: an interaction goes in, something a user can see comes out. Assert on rendered text or elements, not on values you read out of a store.
Explain the gap the state assertion skips — subscription, selector, conditional render, formatting — and make the dominance argument: if the DOM is right the state must have been, so the DOM assertion covers both.
Show where the boundary sits in a real codebase: stores get their own exhaustive unit tests, components get wiring coverage, and a component whose visible outcome is hard to assert is telling you its responsibilities are split wrong.
Own the split at design level — which layers carry their own contract tests, what belongs in a component test versus a store test versus an end-to-end run, and how you stop teams from double-asserting the same behaviour at three levels.
## The shape of the mistake ```jsx await user.click(screen.getByRole('button', { name: /add to cart/i })); expect(useCartStore.getState().items).toHaveLength(1); ``` It reads like a reasonable assertion. It is precise, it is fast, it does not care about copy. And it will happily stay green while the cart badge shows zero to every user of the application. ## Why it is weaker than it looks A component test exists to check one transformation: *interaction in, rendered output out*. The store is an intermediate value inside that transformation. Asserting on the intermediate value verifies the first half and assumes the second. Everything in the second half is a real, common bug: - the component subscribes to a different slice of the store, or to the whole store with a selector that memoises the stale value; - the component re-renders but the count lives behind a condition that is false (`items.length > 1`, a loading flag that never clears); - the rendered text is formatted wrong — "NaN items", "undefined in cart"; - the update happens but is immediately overwritten by an effect that resets the store; - the element is rendered into a portal or a branch that is visually hidden. None of these change `useCartStore.getState().items`. All of them break the feature. ## The dominance argument The cleanest way to say this in an interview: **the DOM assertion strictly dominates the state assertion.** If the page renders "1 item in cart", the store must have contained one item — you got the state check for free. The reverse does not hold. Two assertions where one implies the other is not extra safety; it is one real assertion plus one that can only ever produce false confidence. ```jsx // passes even when <Cart> never re-renders expect(useCartStore.getState().items).toHaveLength(1); // fails whenever the user would not see the result expect(await screen.findByText('1 item in cart')).toBeInTheDocument(); ``` ## Why people reach for it anyway Usually because the DOM assertion was awkward first. The count is not rendered anywhere queryable, the text is split across elements, the component under test is a container that renders no visible output of its own. Those are worth naming honestly, because each has a better answer than giving up on the DOM: - **Nothing visible to assert.** Then the component may not be the right unit — test the store directly in its own test, and test the component that actually renders the count. - **Text split across nodes.** Use a function matcher with `getByText`, or assert on the container element with `toHaveTextContent`, which normalises across child nodes. - **The value is only in an attribute.** Assert the attribute the user's assistive technology reads, for instance the accessible name of the badge, rather than reaching past the DOM entirely. ## The same mistake in other disguises The store variant is the most common in modern React, but the family is larger: reading a value off a ref the component exposes only for tests, asserting on `container.innerHTML` containing a class name, spying on a state setter to check it was called, or asserting that an internal helper module was invoked. All of them share one property — they check something the user cannot perceive, chosen because it is easier to reach than the thing the user can. ## Where state assertions genuinely belong A store is a unit with its own contract, and that contract deserves its own tests: dispatch an action, assert the resulting state, with no component in sight. Those tests are fast, exhaustive and completely appropriate — the store is what is under test there, not a stand-in for one. Keeping the two separated is what makes each honest: the store test covers reducer logic exhaustively, and the component test covers the wiring the store test cannot see. There is one more legitimate use inside a component test: **arrangement**, not assertion. Seeding the store to put the component into a starting state is fine and often the cleanest setup available. The rule is about what you assert at the end, not what you touch at the beginning. ## Summary Assert what the user perceives; treat state as an implementation detail of getting there. If the visible outcome is hard to assert, that difficulty is information about the component, not permission to assert on the store.
- The store is shared across tests. What else can make a state assertion pass falsely?Leaked state from an earlier test. Module-level stores survive between test cases unless something resets them, so `items` may already have had an entry before the click — the length assertion then passes without the interaction doing anything. A DOM assertion on a freshly rendered tree is less exposed to this, but the real fix is resetting the store in setup.
- Where does asserting on a mock callback prop, such as expect(onAddToCart).toHaveBeenCalledWith(id), fit?It is legitimate when the callback *is* the component's output — a presentational component whose whole contract is "tell the parent what was clicked" has nothing else to assert. It becomes the same mistake when the component also renders the result and you assert the call instead of the render, because you are again checking the message rather than the effect.
- How would you test that a value the user cannot see, like an analytics event, fires on click?Assert at the boundary the value actually crosses — the analytics client's method, stubbed for the test — because that boundary is the component's real contract for that behaviour, not an internal detail. Keep it as a separate assertion from the visible outcome, and keep the visible outcome asserted too.
saying these in an interview costs you the question
- If the state is right, the UI must be right
- Asserting the DOM is slower, so check state instead
- Store assertions are more precise, therefore stronger
- The component is just a view, so nothing can break in it
- Both assertions are worth keeping for extra safety