Testing
You will learn how frontend code actually gets tested: the Testing Library family, what a component test should assert, how to fake the network, and how to catch visual and accessibility regressions. Interviewers dig here because most candidates can name a test runner but cannot explain what makes a UI test survive a refactor.
on this pageshowhide
explore
- React Testing Library25 questions
- Query Priority & Accessible Queries5 questions
- user-event vs fireEvent5 questions
- Async Queries, waitFor & act6 questions
- Render, Cleanup & Provider Wrappers5 questions
- Common RTL Mistakes4 questions
- Vue Testing Library4 questions
- React Test Renderer4 questions
- Component Testing Philosophy19 questions
- Behavior Over Implementation5 questions
- Testing Custom Hooks4 questions
- Snapshot Testing Tradeoffs5 questions
- Accessibility Assertions & axe-core5 questions
- End-to-End Testing Patterns26 questions
- Resilient Selector Strategy6 questions
- Waiting & Flakiness Management5 questions
- Auth & Session Handling5 questions
- Page Objects vs Locator-First5 questions
- Test Data & Seeding Strategy5 questions
- Network Mocking19 questions
- Mocking Layers: Module vs Network4 questions
- MSW Service-Worker & Node Interception5 questions
- Fixtures & Contract-Shaped Stubs5 questions
- Error & Latency Simulation5 questions
- Visual Regression Testing20 questions
- Screenshot Diffing & Thresholds5 questions
- Storybook-Based Visual Testing5 questions
- Cross-Browser & Viewport Matrices6 questions
- Handling Nondeterminism4 questions
questions
117 · 7 sectionsA React component renders a user's name only after a fetch promise resolves. In a React Testing Library test, which query do you use to assert that the name appeared, and what must you do with the value that query returns?
basics
~20 sUse an async findBy* query, such as screen.findByText('Ada Lovelace'), and await it. It returns a promise that retries the query until the element appears or a default 1000 ms timeout expires; unawaited, you assert on a promise instead of the DOM.
A React component test suite queries almost every element with screen.getByTestId(...). Why does React Testing Library treat test ids as a last resort, and what does a test-id-only suite fail to catch?
basics
~20 sTest ids are invisible to users, so a test-id-only suite proves markup exists rather than that the interface works. Querying by role, label or visible text makes the query itself verify the text and accessible name a real user relies on.
React Testing Library documents a recommended priority order for its queries. What is that order, and why does getByRole sit at the top while getByTestId sits at the bottom?
basics
~10 sQueries are ranked by how closely they mirror how a real person finds a control: getByRole with an accessible name first, then getByLabelText, getByPlaceholderText, getByText and getByDisplayValue, then getByAltText and getByTitle, and getByTestId last.
In Testing Library, what is the difference between fireEvent.click(button) and userEvent.click(button), and why is user-event the recommended default?
basics
~20 sfireEvent dispatches one synthetic DOM event. user-event replays the whole sequence a real interaction produces — pointer, mouse, focus and click events — and refuses to interact with elements a user could not touch, so it catches bugs fireEvent silently passes.
A React Testing Library test logs "An update to Profile inside a test was not wrapped in act(...)". What is that warning actually reporting, and what in the test usually causes it?
basics
~20 sThe warning reports that a component's state updated while React was outside an act() scope — almost always a promise resolving after the test's synchronous body finished. React Testing Library already wraps render, events and its async utilities in act, so the real fix is awaiting the UI change the promise causes.
In a Vue 3 component test written with Vue Testing Library, why does `await fireEvent.click(getByRole('button', { name: 'Add' }))` need the await before you assert that the new item is on screen?
basics
~20 sVue applies DOM updates asynchronously on the next tick rather than during the event handler. Vue Testing Library's fireEvent returns a promise that waits for that flush, so skipping the await asserts against the old DOM.
Vue Testing Library is built on top of Vue Test Utils. Compared with calling Vue Test Utils' `mount()` directly, what does it deliberately stop your test from touching, and why is that treated as a feature rather than a limitation?
basics
~20 sVue Testing Library's render() hands back DOM queries instead of a Vue Test Utils wrapper, so there is no component instance, no setData, and no CSS-selector find to assert on. A test can only touch rendered output, which is what survives refactoring.
A Vue 3 component's entire contract is emitting a `submit` event with the form payload — it renders no result of its own. How do you test that with Vue Testing Library, and does asserting on emitted events contradict its user-centric principle?
basics
~20 sDrive the real form through its accessible controls, then assert on the recorded event: the render result exposes emitted(), keyed by event name with the arguments of each emission. An emitted event is the component's public contract with its parent, not an internal detail.
A Vue 3 component you want to test with Vue Testing Library uses a router link, a store, and a value supplied through `provide`/`inject`. How do you get it rendering without reaching into its internals, and what do you fake versus supply for real?
basics
~20 sSupply the dependencies through render's mounting options — plugins for the router and store, provide for injected values — rather than reaching into the instance. Give real instances where you can, and fake only what crosses the network or cannot run in the test environment.
The react-test-renderer package renders a React component without any DOM. What does `TestRenderer.create(<Profile />).toJSON()` return, and why would a project want a renderer that produces that instead of DOM nodes?
basics
~20 stoJSON() returns a plain JavaScript object tree of the host elements React produced — each node has type, props and children, with text as plain strings. No DOM node is created, so the renderer runs anywhere React runs, including React Native.
A React 19 web app's test suite still renders components with the react-test-renderer package, and CI now prints a deprecation warning for it. Why was that renderer deprecated for web testing, and what do you move those tests to?
basics
~20 sreact-test-renderer renders to fake host objects rather than to a DOM, so web tests never exercise the environment users get. React 19 deprecates it and logs a warning; move web tests to @testing-library/react and React Native tests to @testing-library/react-native.
A React component suite uses the react-test-renderer package and triggers behaviour by calling props on the returned tree, such as `renderer.root.findByType(SaveButton).props.onClick()`. Every test is green while the Save button is unusable in the browser. Which classes of defect can a renderer with no DOM never catch?
basics
~20 sAnything the host owns stays invisible: real event dispatch and bubbling, disabled or covered controls, focus and keyboard operation, form semantics, CSS-driven visibility and layout, and the accessibility tree. Calling a prop bypasses every gate a real user would have to pass.
Under the react-test-renderer package, a React component whose effect runs `inputRef.current.focus()` blows up on a null ref, though the same component works in the browser. Why is the ref null there, and how do you get such a component to render under that renderer?
basics
~20 sThere is no DOM node to attach, so react-test-renderer sets host refs to null by default. Pass the createNodeMock option to TestRenderer.create to return a stand-in object per host element — but a mocked focus() proves nothing about real focus behaviour.
Your component test suite runs an automated accessibility rule check on every component and reports zero violations. What can you honestly claim from that, and which accessibility problems does it leave undetected?
basics
~20 sZero violations only means no enabled rule found a machine-decidable markup defect. Automated rules cannot judge whether names and alt text are meaningful, whether focus order and visibility work, or whether announcements arrive usefully — those need manual keyboard and screen-reader checks.
In a UI component test, what counts as an implementation detail, and what concrete rule do you use to decide whether a given assertion will survive a refactor?
basics
~20 sAn implementation detail is anything a user cannot perceive: internal state, render counts, DOM structure, class names, which child components exist. The working rule is to assert only what a user would notice if it stopped being true.
You have a custom React hook `useCart` that several components use. When is it right to test it directly with React Testing Library's `renderHook`, and when should you test it through a component that consumes it?
basics
~20 sTest a hook directly when its return value is the product: a shared hook whose callers are other developers. Test it through a consuming component when the hook exists to serve one component, because there the rendered output is what people depend on.
In a component test written with Testing Library and Jest or Vitest, how does a snapshot assertion such as expect(container).toMatchSnapshot() differ from a targeted assertion such as expect(screen.getByRole('heading')).toHaveTextContent('Invoice'), and when does the snapshot stop earning its place?
basics
~20 sA snapshot asserts that the whole serialized output is identical to a recording; a targeted assertion states one thing a human decided must be true. Snapshots catch every change including irrelevant ones, so once they fail routinely for reasons unrelated to behaviour, they stop being evidence and become a rubber stamp.
In a frontend component test, what does an automated accessibility check such as jest-axe's axe(container) followed by expect(results).toHaveNoViolations() actually do, and what does a passing result prove?
basics
~20 sAn automated accessibility check evaluates a fixed set of rules against the component's rendered DOM and fails the test if any rule is violated. Passing means no enabled rule fired — not that the component is accessible.
An end-to-end suite signs in through the login form at the start of every test, and a full run now takes twenty minutes. Why is UI login per test a poor default, and what do end-to-end suites do instead?
basics
~20 sDriving the login form repeats a slow, flake-prone flow that adds no coverage after the first test. Authenticate once through the app's auth API instead, save the resulting cookies and browser storage as a session, and start every test already signed in.
An end-to-end browser test clicks a button using the CSS selector `.card > div:nth-child(2) > button.btn-primary`. It passes today and fails after a refactor that changed only markup nesting and class names, with no change to what a user can do. What makes that selector fragile, and what should the test anchor on instead?
basics
~20 sA structural CSS path couples the test to DOM nesting and styling class names, which change freely without changing behaviour. Anchor on what a user perceives — the element's role and visible name — or on an explicit test-id attribute the component deliberately exposes.
In a browser end-to-end test, why is inserting a fixed sleep (such as Playwright's page.waitForTimeout(500) or Cypress's cy.wait(500)) before an assertion a bad way to handle timing, and what do you use instead?
basics
~20 sA fixed sleep hard-codes a guess about duration: too short and the test fails on a slow machine, too long and every run pays the delay. Wait on a retrying condition instead, which returns the moment the expected state appears.
In an end-to-end browser test suite, what belongs inside a page object and what should stay in the test — and why do experienced teams keep assertions out of page objects?
basics
~20 sA page object owns a screen's locators and user actions and returns data or locators; the test keeps the assertions and the scenario. Assertions buried in a shared helper hide what each test actually checks and make one test's expectations everyone's.
In an end-to-end browser test, why do many teams locate elements by accessible role and visible name — for example a button named "Save changes" — before trying any other strategy, and where does that strategy break down?
basics
~20 sRole plus name is how a user identifies a control, so it changes only when the interface genuinely changes, and a test that cannot find the element usually reveals a real labelling gap. It breaks on unlabelled icon controls, non-semantic markup, duplicated names, and copy or locale churn.
A component fetches a list from an API, and every test in its suite stubs that call with a successful response. What is left untested, and how would you write a test that covers the failure path?
basics
~20 sHappy-path stubs leave the error and empty branches unexecuted, so bugs there ship unnoticed. Override the stub in one test so the endpoint fails, then assert the visible error message appears and the list does not.
In Mock Service Worker (MSW) v2 a request handler is written as http.get('/api/users', resolver). What are the two halves of that handler, what must the resolver return, and does the component's own fetch() code have to change?
basics
~20 sAn MSW handler pairs a predicate (method plus path, e.g. http.get('/api/users')) with a resolver function that returns a Response, usually built with HttpResponse.json(). Application code is untouched — MSW answers the real request at the network boundary.
A test stubs a component's API call with an instantly-resolved response, so its loading skeleton disappears before any assertion can run. How do you make the pending state observable in a test without adding a fixed sleep?
basics
~20 sTake control of when the stubbed response settles: either inject a delay into the handler, or hand the test a promise it resolves by hand. Assert the skeleton while the request is pending, release the response, then assert it is replaced by the data.
Your frontend tests stub API responses with hand-written JSON fixtures. How do you make the build fail when the backend's OpenAPI or GraphQL schema renames a response field, and where does that protection stop?
basics
~20 sGenerate types from the API schema as a build step and annotate every fixture with the generated type, so a renamed or removed field fails typecheck. That only catches shape changes present in the schema copy you last regenerated — never runtime values or semantics.
Why can a frontend test that mocks the application's own API-client module stay green while the feature is broken for real users, and what changes when the fake is moved down to the network boundary instead?
basics
~20 sMocking your own client deletes the code that talks to the server, so the test can only confirm your assumptions about it. Faking at the network boundary keeps that code running and replaces only the server, so request shape, parsing and error handling are exercised for real.
A visual regression suite covers 40 pages across 3 browser engines, 4 viewport widths, 2 colour schemes and 2 device-pixel-ratio settings. How many baseline images is that, and what does each extra axis cost beyond image storage?
basics
~20 sForty-eight configurations per page, so 1,920 baselines — the axes multiply rather than add. The real cost is not disk: it is CI time, a multiplied flake surface, and above all the number of images a human must review and approve on every intentional design change.
A dashboard screenshot test fails on every run: the page shows "updated 3 minutes ago", a randomly generated order ID, and a third-party map tile. How do you make each of those stable, and when would you mask a region instead?
basics
~20 sMake the data deterministic at its source: freeze the clock and pin the timezone and locale, seed or stub the ID generator, and serve fixed fixtures. Mask only what you truly cannot control, such as a live third-party tile, because a mask is a permanent blind spot.
In a visual regression test, a card plays a 300 ms entry animation before it settles. Why is inserting a fixed delay before the screenshot a poor fix, and what would you do instead?
basics
~20 sA fixed delay is a guess: on a loaded CI machine the shutter still lands mid-animation, and on a fast one it only wastes time. Remove the motion instead — inject test-only CSS that disables animations and transitions and hides the text caret.
In a visual regression suite, why does requiring two screenshots of the same page to match exactly fail almost immediately, and what does a perceptual per-pixel comparison do instead?
basics
~20 sByte-equal screenshots almost never repeat: GPU rasterisation, text shaping and anti-aliasing shift pixel values without changing what a human sees. Visual tools instead decode both images, score each pixel pair with a perceptual colour distance, and count only differences above a tolerance.
Screenshot baselines captured on a developer's macOS laptop fail on the Linux CI runner with tiny anti-aliasing differences across every piece of text. Why does that happen, and how would you set the suite up so baselines are portable?
basics
~20 sText rasterization is operating-system specific — hinting, anti-aliasing and the installed fallback fonts all differ — so a macOS baseline is simply not valid on Linux. Produce and compare baselines inside one pinned container image, used identically in CI and on developer machines.