Irbisa · Cheatsheet

Testing React

React Testing Library, Vitest and Jest, queries and user-event, mocking the network with MSW, testing hooks, Playwright end-to-end

Middle Developer · 21 questions

  1. 01

    What is React Testing Library, and what does "test behaviour, not implementation details" mean in practice?

    React Testing Library (RTL) renders a component into a DOM (jsdom in Vitest or Jest) and lets you query and interact with it the way a user does: by role, label and text.

    import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { Counter } from './Counter'; test('increments the count when the button is clicked', async () => {  const user = userEvent.setup();…
  2. 02

    What is the difference between unit, integration and end-to-end tests in a React app, and how much of each would you write?

    A unit test checks one function or hook alone, an integration test renders a component tree with real children and state and fakes only the network, and an E2E test drives the whole app in a browser.

  3. 03

    What is TDD, and when does writing the test first pay off in frontend work?

    Test-driven development is a short loop: write a test for the next piece of behaviour and watch it fail (red), write the least code that makes it pass (green), then clean the code up with the test as a safety net (refactor).

  4. 04

    Vitest or Jest: which would you pick for a React project in 2026, and how do you set it up?

    For a Vite project, Vitest: it reuses the Vite config and transforms, runs ESM and TypeScript without Babel, and keeps a Jest-compatible API (describe, it, expect, vi.fn instead of jest.fn).

    // vite.config.tsimport { defineConfig } from 'vitest/config';import react from '@vitejs/plugin-react'; export default defineConfig({  plugins: [react()],…
  5. 05

    What is the difference between getBy, queryBy and findBy queries in React Testing Library?

    getBy returns the element or throws if it is missing, queryBy returns null when it is missing, and findBy returns a promise that retries until the element appears or the timeout (1000 ms by default) runs out.

    test('shows the user after loading', async () => {  render(<Profile id={1} />);   expect(screen.getByRole('progressbar')).toBeInTheDocument(); // there now  expect(await screen.findByRole('heading', { name: 'Ada' })).toBeInTheDocument(); // appears later  expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); // gone…
  6. 06

    Which RTL query should you reach for first, and why is getByTestId the last resort?

    Start with getByRole plus an accessible name, then getByLabelText for form fields, then getByText; getByTestId is for elements a user cannot identify any other way.

  7. 07

    Find the bug: the test that checks an error message is gone fails with "Unable to find an element".

    test('clears the error after a valid email', async () => {  const user = userEvent.setup();  render(<SignupForm />);   await user.type(screen.getByLabelText('Email'), 'nope');  await user.tab();  expect(screen.getByText('Enter a valid email')).toBeInTheDocument();   await user.clear(screen.getByLabelText('Email'));  await user.type(screen.getByLabelText('Email'), 'ada@example.com');  expect(screen.getByText('Enter a valid email')).not.toBeInTheDocument();});

    getByText throws as soon as the element is missing, so the assertion never runs: the absence check must use queryByText, which returns null and lets not.toBeInTheDocument() pass.

  8. 08

    What is the difference between userEvent and fireEvent, and why is userEvent preferred?

    fireEvent dispatches one synthetic DOM event, while userEvent simulates the whole interaction a browser produces: a click is pointer, mouse and focus events, typing is keydown, input and keyup for every character.

    test('shows an error when the user leaves the email field empty', async () => {  const user = userEvent.setup();  render(<EmailField />); // validates on blur   await user.click(screen.getByLabelText('Email')); // pointer events, then focus  await user.tab(); // focus moves on, so the field blurs…
  9. 09

    How do you test asynchronous UI, and what does the warning "An update to X inside a test was not wrapped in act(...)" mean?

    Await the final state the user would see, with findBy* or await waitFor(...).

  10. 10

    Find the bug: this test passes even when the component never shows "Saved".

    test('shows a confirmation after save', async () => {  const user = userEvent.setup();  render(<Settings />);   await user.click(screen.getByRole('button', { name: /save/i }));   waitFor(() => {    expect(screen.getByText('Saved')).toBeInTheDocument();  });});

    waitFor returns a promise and nothing awaits it, so the test function finishes right after the click and is reported as passed; the failing assertion runs later, after the test is over.

  11. 11

    How do you mock network requests in component tests, and why is MSW usually better than mocking fetch or the API module?

    Mock Service Worker (MSW) intercepts requests at the network level and answers them from handlers you define, so the component, its data library and the real fetch code all run unchanged.

    // src/test/server.tsimport { http, HttpResponse } from 'msw';import { setupServer } from 'msw/node'; export const server = setupServer(  http.get('/api/user/:id', ({ params }) =>…
  12. 12

    How do you test a component that needs providers such as a TanStack Query client, a router or a theme?

    Write a custom render helper that wraps the UI in the same providers the app uses, creating fresh instances for every test, and use it instead of RTL's bare render.

    import { render } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import { MemoryRouter } from 'react-router';import type { ReactElement } from 'react';…
  13. 13

    How do you test a custom hook?

    If the hook is used by one component, test it through that component.

    import { renderHook, act } from '@testing-library/react';import { useCounter } from './useCounter'; test('increments and resets when the start value changes', () => {  const { result, rerender } = renderHook(({ start }) => useCounter(start), {    initialProps: { start: 5 },…
  14. 14

    What do vi.mock, vi.fn and vi.spyOn do, and when is mocking a module a bad idea?

    vi.fn creates a standalone mock function, vi.spyOn wraps an existing method so you can watch or replace it and restore it later, and vi.mock swaps a whole module for every import in the test file.

    import { vi, test, expect, afterEach } from 'vitest';import * as analytics from './analytics';import { checkout } from './checkout'; afterEach(() => vi.restoreAllMocks());…
  15. 15

    Find the bug: the test file crashes with a ReferenceError before any test runs.

    import { vi, test, expect } from 'vitest';import { getUser } from './api';import { greet } from './greet'; const fakeGetUser = vi.fn().mockResolvedValue({ name: 'Ada' });vi.mock('./api', () => ({ getUser: fakeGetUser })); test('greets the user', async () => {  expect(await greet(1)).toBe('Hello, Ada');  expect(getUser).toHaveBeenCalledWith(1);});

    vi.mock is hoisted to the top of the file, above the const fakeGetUser line, so when the factory runs during the import of ./greet, fakeGetUser is still in its temporal dead zone.

  16. 16

    How do you test code that depends on timers, such as a debounced search input?

    Switch to fake timers with vi.useFakeTimers(), tell userEvent to advance them (advanceTimers: vi.advanceTimersByTime), then move the clock forward explicitly inside act.

    afterEach(() => vi.useRealTimers()); test('searches 300 ms after the user stops typing', async () => {  vi.useFakeTimers();  const onSearch = vi.fn();  const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });…
  17. 17

    When are snapshot tests useful in a React codebase, and why do many teams avoid large component snapshots?

    Snapshots suit small, stable outputs where any change deserves a look: a serializer, an error message, ideally as toMatchInlineSnapshot.

  18. 18

    What does code coverage tell you, and why is 100% coverage a poor target for a React codebase?

    Coverage reports which lines, branches and functions ran during the tests.

    // vite.config.tsimport { defineConfig } from 'vitest/config'; export default defineConfig({  test: {    coverage: {…
  19. 19

    What is Playwright, which flows would you cover with it, and how does it avoid the flakiness older E2E tools were known for?

    Playwright drives real Chromium, Firefox and WebKit from a test runner and checks the built app the way a user does.

    import { test, expect } from '@playwright/test'; test('user signs in and sees the dashboard', async ({ page }) => {  await page.goto('/login');   await page.getByLabel('Email').fill('ada@example.com');…
  20. 20

    How do you test Server Components and Server Actions, given that Testing Library cannot render an async Server Component?

    Split the work by layer: test Server Actions and data functions as plain async functions in Vitest, test Client Components with RTL by passing them props and a fake action, and cover the assembled page, where async components and the RSC protocol run, with Playwright against a production build.

    import { beforeEach, expect, test, vi } from 'vitest';import { revalidatePath } from 'next/cache';import { auth } from '@/lib/auth';import { db } from '@/lib/db';import { renamePost } from './actions';…
  21. 21

    Your React test suite is slow and some tests fail randomly in CI. How do you find the causes and fix them?

    Random failures almost always come from shared state, real time or unawaited async work; slowness, from rendering too much per test.