React Testing Library, queries, user events, async assertions, mocking network requests, testing hooks and what is worth testing at all.
10 questions9 mid-level, 1 senior
Testing questions are a shortcut to seeing how you work. The specific API matters far less than the philosophy: are you asserting on what a user can observe, or on how the component happens to be built. Every answer in this section is stronger if you tie it back to that.
1.What is React Testing Library and how does it differ from Enzyme?
Mid-level
React Testing Library renders components and queries them the way a user would, through visible text and accessible roles. Enzyme exposed the component instance, so tests could reach into state and internal methods, which made them break on every refactor.
The guiding principle is that the more your tests resemble the way the software is used, the more confidence they give you. A test that asserts wrapper.state('isOpen') === true passes when the menu is broken and fails when you rename a variable. A test that asserts the menu is visible does the opposite.
RTL has no API for reading state, props or instance methods, deliberately.
It renders into a real DOM through jsdom, so you test the rendered output rather than a shallow tree.
Enzyme is effectively unmaintained and never fully supported React 18, so RTL is the default for new work.
Likely follow-up questions
What would you test in a component then?
2.What is the recommended order of queries in React Testing Library?
Mid-level
Accessible queries first — getByRole, then getByLabelText, getByPlaceholderText and getByText — then getByTestId as a last resort. The order mirrors how a user, including one with a screen reader, finds things.
screen.getByRole('button', { name: /save/i });
screen.getByLabelText('Email address');
screen.getByText('No results found');
screen.getByTestId('chart-canvas'); // only when nothing else works
getBy throws when there is no match, queryBy returns null and is what you use to assert absence, findBy returns a promise and retries.
Prefer getByRole with a name option: it fails when a button loses its accessible name, which is a real bug.
A test id is fine for something with no accessible representation, such as a canvas, but a test file full of them usually means the markup is not accessible.
Likely follow-up questions
How do you assert that something is not on the page?
What is the difference between getBy and findBy?
3.How do you test asynchronous behaviour?
Mid-level
Use findBy queries or waitFor, both of which retry until the assertion passes or the timeout expires. Never assert immediately after an action that triggers an async update, and never reach for a fixed setTimeout.
await user.click(screen.getByRole('button', { name: /load/i }));
// Retries until it appears
expect(await screen.findByText('3 results')).toBeInTheDocument();
// For assertions that are not a query
await waitFor(() => expect(onLoad).toHaveBeenCalledTimes(1));
Likely follow-up questions
What is the act warning telling you?
4.What is the difference between fireEvent and userEvent?
Mid-level
fireEvent dispatches a single DOM event. userEvent simulates the full interaction a real user produces — for a click that is pointer, mouse down, focus, mouse up and click — so it catches bugs fireEvent walks straight past.
userEvent.type fires a keydown, keypress, input and keyup per character, so a field that only handles keydown behaves realistically. It also refuses to click a disabled element, which fireEvent will happily do. Use userEvent by default; drop to fireEvent only for events a user cannot produce, such as scroll.
Likely follow-up questions
Why does userEvent need await?
5.How do you mock network requests in tests?
Mid-level
Intercept at the network layer with Mock Service Worker rather than stubbing fetch or the module that calls it. The component then runs its real data code and the test stays valid when you change HTTP client.
Handlers can be overridden per test to return a 500 or a slow response, so error and loading states are actually covered.
The same handlers work in tests and in a development environment with no backend.
jest.mock('./api') couples the test to your module layout and stops testing the code that builds the request.
Likely follow-up questions
How would you test the error state?
6.How do you test a custom hook?
Mid-level
With renderHook from React Testing Library, wrapping state updates in act. If the hook only exists to serve one component, testing it through that component is often more valuable.
result.current is re-read after each render, so hold onto result rather than destructuring values out of it. Pass a wrapper option when the hook needs a provider above it.
7.What should you actually test in a React component?
Mid-level
Behaviour a user depends on: what renders for a given set of props, what happens when they interact, and what the component does in loading, empty and error states. Not implementation details such as state variable names or which hook fired.
Renders the right thing for representative props, including the empty and error cases people forget.
Interaction produces the expected visible change or calls the expected callback.
Conditional and accessibility-relevant attributes: disabled states, aria-invalid, focus after an action.
Not: internal state, the number of renders, whether useMemo was used, or exact CSS classes.
Likely follow-up questions
How much coverage is enough?
8.What is the difference between unit, integration and end to end tests in a React project?
Mid-level
Unit tests cover a single function or component in isolation, integration tests cover several components working together with mocked network calls, and end to end tests drive the real application in a browser against a real or seeded backend.
Speed
Confidence
Typical tool
Unit
Milliseconds
Low on its own
Vitest or Jest
Integration
Fast
High per unit of effort
RTL plus MSW
End to end
Slow, flaky if careless
Highest
Playwright or Cypress
The pragmatic position, and the one most interviewers share, is that component integration tests are the sweet spot in React: they exercise real user flows without the cost of a browser, and a small number of end to end tests then cover the critical paths such as sign in and checkout.
Likely follow-up questions
Where would you put the majority of your tests?
9.What causes the "not wrapped in act(...)" warning?
Senior
A state update happened outside anything React was told to wait for, usually an async update that resolved after the test finished its assertions. The fix is almost always to await the resulting change rather than to wrap something in act.
// Warns: the fetch resolves after the test has moved on
render(<Profile id='1' />);
expect(screen.getByText('Loading')).toBeInTheDocument();
// Fixed: wait for the update the component will make
render(<Profile id='1' />);
expect(await screen.findByText('Ada')).toBeInTheDocument();
act tells React to flush effects and updates before you assert. Testing Library already wraps its own render and userEvent calls, so a warning usually means there is an update in flight that the test never waited for — which is a real gap, not just noise.
Likely follow-up questions
Why is silencing the warning a bad idea?
10.How do you test a component that depends on context, a router or a store?
Mid-level
Write a custom render that wraps the component in the same providers the app uses, and export it instead of RTL's render. Every test then gets the real environment without repeating setup.