Common React Testing Mistakes and How to Avoid Them

Stop testing implementation details, misusing queries, and ignoring async so your React test suite stays fast, readable, and trustworthy.

7 min read

Most React test failures come from the same handful of habits. This guide lists the common React testing mistakes, why each one hurts, and the smaller fix that keeps a suite fast and trustworthy. Each mistake is a behavior that a future refactor or async change will eventually expose.

Testing implementation details

A test that reaches into props, state, or internal function calls breaks the moment the component is refactored, even when the user sees no change.

App.jsxApp.jsx
// BAD: reaches into internals
expect(component.props.onSave).toHaveBeenCalled();
 
// GOOD: checks what the user sees
expect(screen.getByText("Saved")).toBeVisible();

Assert on the rendered result instead of how the component got there. A save button that moves its handler into a custom hook still shows the same confirmation, so the good test keeps passing. For the philosophy behind this, start with the React Testing Library tutorial.

Querying by test ID everywhere

Test IDs are invisible to users and assistive technology, so a test built on them proves little about the real experience.

App.jsxApp.jsx
// BAD
screen.getByTestId("submit-button");
 
// GOOD
screen.getByRole("button", { name: "Submit" });

The role query matches the accessible name a screen reader announces, and it keeps working when the markup changes. Reserve test IDs for elements with no meaningful text, such as spinners. See how to query elements in React Testing Library for the full priority order.

Using queryBy for existence

The queryBy variants return null instead of throwing, which is useful only when asserting absence.

App.jsxApp.jsx
// BAD
expect(screen.queryByRole("alert")).toBeInTheDocument();
 
// GOOD
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();

getBy throws a helpful error that prints the rendered DOM when the element is missing. queryBy gives a terse null message, so a failing existence check tells you nothing about what went wrong.

Using fireEvent for normal interactions

fireEvent dispatches one event and stops there, while a real click or keystroke produces a whole sequence.

App.jsxApp.jsx
// BAD
fireEvent.change(input, { target: { value: "Ada" } });
 
// GOOD
await user.type(input, "Ada");

The type method focuses the field, fires the key events, and updates the value the way a browser would. A component that listens for keydown or focus can pass a fireEvent test and still break for real users. See how to test user events in React.

Racing async updates

A get query throws when the element is not there yet, so components that fetch data need a query that retries.

App.jsxApp.jsx
// BAD
screen.getByText("Ada");
 
// GOOD
await screen.findByText("Ada");

findBy retries until the element appears or the timeout elapses. Missing await is the same mistake in disguise, because the test finishes before React processes the update. Either way, the assertion runs before the component has caught up.

Silencing act warnings

An act warning means an update happened outside React's awareness. Wrapping every call in act or suppressing the warning hides the real problem instead of fixing it.

Most warnings disappear when tests use the async utilities, user-event, and the state updates those helpers already wrap. When a warning remains, it usually points to a genuinely unexpected update, such as a fetch resolving after unmount.

Keeping tests independent

Tests that share rendered DOM or mutable mocks leak state into each other. Testing Library auto-cleans between tests when the runner supports it, so a failing test that only fails after another test ran is usually a shared-mock problem.

Reset mocks in afterEach and let cleanup unmount each tree. An isolated test is the difference between a flaky suite and one you can trust.

Common mistakes at a glance

  • Testing props, state, or calls instead of visible output.
  • Using test IDs when a role or label matches.
  • Using queryBy for anything other than absence.
  • Using fireEvent for ordinary clicks and typing.
  • Forgetting await on findBy and waitFor.
  • Silencing act warnings instead of removing the stray update.

What to learn next

The fixes here build on the core Vitest workflow, and the async side of the same mistakes has its own guide. Review those two, then apply the patterns to every new test you write.

Rune AI

Rune AI

Key Insights

  • Test visible output and interactions, not state, props, or function calls.
  • Query by role and label first, and reserve test IDs for the rare exceptions.
  • Use getBy for existence, queryBy for absence, and findBy for async.
  • Prefer user-event over fireEvent for realistic interactions.
  • Never silence act warnings; fix the missing act instead.
RunePowered by Rune AI

Frequently Asked Questions

What is the single most common React testing mistake?

Testing implementation details such as state, props, or function calls instead of the visible output. Those tests break on every refactor without catching real bugs.

How do I stop seeing act warnings in tests?

Read the warning as a signal that an update happened outside React's awareness. Use the async utilities and user-event, which already handle act, instead of silencing the warning.

Conclusion

Most React testing mistakes trace back to one habit: asserting on internals or racing updates. Query like a user, await async work, and let the visible UI drive every assertion.