Irbisa · Шпаргалка

Тестирование React

React Testing Library, Vitest и Jest, запросы и user-event, мок сети через MSW, тесты хуков, end-to-end на Playwright

Middle-разработчик · 21 вопрос

  1. 01

    Что такое React Testing Library и что на практике означает «тестировать поведение, а не детали реализации»?

    React Testing Library (RTL) рендерит компонент в DOM (jsdom в Vitest или Jest) и даёт искать элементы и взаимодействовать с ними, как пользователь: по роли, подписи и тексту.

    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

    Чем отличаются unit-, интеграционные и end-to-end тесты в React-приложении и сколько каких вы бы писали?

    Unit-тест проверяет одну функцию или хук изолированно, интеграционный рендерит дерево компонентов с настоящими детьми и стейтом и подменяет только сеть, а E2E прогоняет всё приложение в браузере.

  3. 03

    Что такое TDD, и когда во фронтенде окупается написание теста первым?

    Разработка через тестирование (test-driven development) строится на коротком цикле: пишете тест на следующий кусок поведения и видите, что он падает (red), пишете минимум кода, чтобы он прошёл (green), затем чистите код, опираясь на тест как на страховку (refactor).

  4. 04

    Vitest или Jest: что вы выберете для React-проекта в 2026 году и как его настроить?

    Для проекта на Vite выбирают Vitest: он переиспользует конфиг и трансформации Vite, запускает ESM и TypeScript без Babel и сохраняет совместимый с Jest API (describe, it, expect, vi.fn вместо jest.fn).

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

    Чем отличаются запросы getBy, queryBy и findBy в React Testing Library?

    getBy возвращает элемент или бросает ошибку, если его нет, queryBy возвращает null, если элемента нет, а findBy возвращает промис, который повторяет поиск, пока элемент не появится или не истечёт таймаут (по умолчанию 1000 мс).

    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

    Какой запрос RTL использовать в первую очередь и почему getByTestId остаётся крайним вариантом?

    Начинайте с getByRole плюс доступное имя, затем getByLabelText для полей формы, затем getByText; getByTestId нужен для элементов, которые пользователь никак иначе не опознает.

  7. 07

    Найдите баг: тест, проверяющий, что сообщение об ошибке исчезло, падает с «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 бросает ошибку сразу, как только элемента нет, поэтому до проверки дело не доходит: отсутствие нужно проверять через queryByText, который возвращает null и позволяет not.toBeInTheDocument() пройти.

  8. 08

    Чем отличаются userEvent и fireEvent и почему предпочитают userEvent?

    fireEvent отправляет одно синтетическое DOM-событие, а userEvent имитирует всё взаимодействие, которое производит браузер: клик состоит из событий pointer, mouse и focus, ввод даёт keydown, input и keyup на каждый символ.

    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

    Как тестировать асинхронный UI и что значит предупреждение «An update to X inside a test was not wrapped in act(...)»?

    Дожидайтесь итогового состояния, которое увидит пользователь, через findBy* или await waitFor(...).

  10. 10

    Найдите баг: этот тест проходит, даже когда компонент так и не показывает «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 возвращает промис, и его никто не ждёт, поэтому тестовая функция завершается сразу после клика и считается пройденной; падающая проверка выполняется позже, когда тест уже закончился.

  11. 11

    Как мокать сетевые запросы в тестах компонентов и почему MSW обычно лучше, чем мок fetch или API-модуля?

    MSW (Mock Service Worker) перехватывает запросы на уровне сети и отвечает из ваших обработчиков, поэтому компонент, его библиотека данных и настоящий fetch работают без изменений.

    // 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

    Как тестировать компонент, которому нужны провайдеры, например клиент TanStack Query, роутер или тема?

    Напишите свою функцию рендера, которая оборачивает UI в те же провайдеры, что и приложение, создавая свежие экземпляры для каждого теста, и используйте её вместо голого render из RTL.

    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

    Как протестировать кастомный хук?

    Если хук использует один компонент, тестируйте его через этот компонент.

    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

    Что делают vi.mock, vi.fn и vi.spyOn и когда мокать модуль плохая идея?

    vi.fn создаёт отдельную мок-функцию, vi.spyOn оборачивает существующий метод, чтобы следить за ним или подменить его и потом восстановить, а vi.mock подменяет целый модуль для всех импортов в тестовом файле.

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

    Найдите баг: тестовый файл падает с ReferenceError ещё до запуска первого теста.

    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 поднимается в начало файла, выше строки const fakeGetUser, поэтому когда фабрика выполняется при импорте ./greet, fakeGetUser ещё находится во временной мёртвой зоне (TDZ).

  16. 16

    Как тестировать код, который зависит от таймеров, например поле поиска с debounce?

    Переключитесь на фейковые таймеры через vi.useFakeTimers(), скажите userEvent продвигать их (advanceTimers: vi.advanceTimersByTime), а затем явно сдвигайте часы внутри 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

    Когда снапшот-тесты полезны в React-кодовой базе и почему многие команды избегают больших снапшотов компонентов?

    Снапшоты подходят для небольшого стабильного вывода, где любое изменение стоит просмотреть: сериализатор, текст ошибки, лучше всего как toMatchInlineSnapshot.

  18. 18

    Что показывает покрытие кода (coverage), и почему 100% покрытия не стоит делать целью для React-кодовой базы?

    Покрытие показывает, какие строки, ветки и функции выполнились во время тестов.

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

    Что такое Playwright, какие сценарии вы бы им покрывали и как он избегает нестабильности, которой славились старые E2E-инструменты?

    Playwright управляет настоящими Chromium, Firefox и WebKit из тест-раннера и проверяет собранное приложение так, как это делает пользователь.

    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

    Как тестировать Server Components и Server Actions, если Testing Library не умеет рендерить async Server Component?

    Разделите работу по слоям: Server Actions и функции данных тестируйте как обычные async-функции в Vitest, Client Components через RTL, передавая им пропсы и фейковый action, а собранную страницу, где работают async-компоненты и протокол RSC, покрывайте Playwright на продакшен-сборке.

    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

    Набор тестов React медленный, а часть тестов случайно падает в CI. Как вы найдёте причины и исправите их?

    Случайные падения почти всегда вызваны общим состоянием, реальным временем или асинхронностью без await, а медленность лишним рендерингом в каждом тесте.