javascript

37 советов и приемов по написанию качественных тестов для фронтенда

  • вторник, 31 марта 2026 г. в 00:00:08
https://habr.com/ru/companies/timeweb/articles/1006680/

Мне нравится писать тесты. Написание теста и последующее обновление кода для его прохождения — всегда увлекательный процесс.

Но нет ничего хуже, чем выяснение того, что проверяют существующие тесты (раньше я сам часто был автором таких тестов).

Представляю вашему вниманию мои советы по написанию высококачественных тестов для фронтенда.

Некоторые примеры намеренно упрощены путем удаления специфичных для фронтенда частей для облегчения понимания. Эта статья посвящена тестированию фронтенда, но, честно говоря, большинство принципов чистого тестирования применимы как к фронтенду, так и к бэкенду.

❯ Хорошие тесты четко определяют, что именно они проверяют

При тестировании компонента, отдельные test() (или if()) для разных утверждений (assertions) существенно облегчают поддержку.

Важно отметить: я не настаиваю на строгом правиле «1 тест = 1 утверждение».

Это глупое правило, которое некоторые доводят до абсурда. Но лучше держать несвязанные утверждения в разных вызовах test().

Например, следующий тест неаккуратный и сложный в поддержке, а в случае сбоя неясно, что именно он проверяет:

test('component works ok', async () => {
  render(<SomeComponent />);

  // Тестируем загрузку данных
  expect(
    screen.getByText('Loading...')
  ).toBeInTheDocument();

  await waitFor(() =>
    expect(fetch).toHaveBeenCalled()
  );
  expect(
    await screen.findByText(
      'your mock data from api'
    )
  ).toBeInTheDocument();

  expect(
    screen.queryByText('Loading...')
  ).toBeNull();

  // Тестируем создание
  await userEvent.click(
    screen.getByRole('button', {
      name: 'Create',
    })
  );
  expect(
    await screen.findByText(
      'Created ok message'
    )
  ).toBeInTheDocument();
  expect(fetch).toHaveBeenCalledWith(
    '/create'
  );

  // Тестируем удаление
  await userEvent.click(
    screen.getByRole('button', {
      name: 'Delete',
    })
  );
  expect(
    screen.getByText('Deleted item')
  ).toBeInTheDocument();
  expect(fetch).toHaveBeenCalledWith(
    '/delete'
  );
});

В этом тесте мы:

  • тестируем загрузку данных (вызов fetch())

  • и возможность использования формы создания

  • и возможность использования формы удаления

Это немного надуманный пример, но добавить подобные тесты очень легко (особенно при добавлении небольших функций: проще обновить существующий тест и протестировать небольшую новую функцию в том же тесте, чем писать отдельный).

Поддерживать такие тесты сложно, потому что в случае сбоя не сразу понятно, какая именно часть вышла из строя.

В этом примере я бы разделил тест на три части. Затраты на три разных теста, каждый из которых будет выполняться отдельно, оправданы, поскольку это улучшит опыт разработки и сделает тесты более понятными.

❯ Избегайте тестирования деталей реализации

Старайтесь избегать тестирования деталей реализации. В тестировании вне фронтенда мы всегда стараемся тестировать публичные методы классов. Во фронтенд-тестировании мы используем тот же принцип, но тестируем все через то, что отображают наши компоненты, и взаимодействие, которое мы можем с ними осуществлять (через кнопки).

Казалось бы, что это очевидно, поскольку об этом часто упоминается, но очень легко увлечься и начать тестировать детали реализации, а не поведение пользователей (реальных пользователей UI или публичного API).

Пример — тестирование хука и тестирование компонента, использующего этот хук:

// ❌ Плохо - тестирование деталей реализации
test('component uses useState correctly', () => {
  const { result } = renderHook(() =>
    useState(0)
  );
  expect(result.current[0]).toBe(0);
});

// ✅ Хорошо - тестирование пользовательского поведения
test('counter displays initial value of 0', () => {
  render(<Counter />);
  expect(
    screen.getByText('Count: 0')
  ).toBeInTheDocument();
});

Если тестировать только хук, нет гарантий, что компонент работает, как ожидается.

Это также означает, что при рефакторинге компонента (тот же функционал, но другой способ увеличения счетчика, например, с помощью другого хука), существующий тест будет проходить.

Обратите внимание: бывают случаи, когда имеет смысл тестировать не только компонент, но также некоторые особенности реализации в изоляции. Возможно, наш компонент использует хук, и мы хотим протестировать различные комбинации возвращаемых им значений. Но помните, что если мы заменим этот хук на что-то другое, наш компонент будет недостаточно покрыт тестами.

❯ Тестируйте логику своего приложения, а не сторонний код

Не тестируйте код, специфичный для конкретного фреймворка или библиотеки (React, Next).

Если вы используете фреймворк или библиотеку, то их тестирование не принесет особой пользы.

Разумеется, следует тестировать свой код, который их использует.

Для приложений React, например, это означает следующее:

  • не стоит тратить время на тестирование возможности обработки и рендеринга JSX-файлов

  • при передаче style={someStyleObject}, нет смысла проверять, правильно ли React устанавливает стили

Менее понятной является ситуация с проверкой того, как Next переключается между страницами. Зачастую единственный надежный способ — запуск сквозных тестов (Playwright или Cypress) и проверка изменения URL-адреса.

❯ Использование правильных (наиболее подходящих) функций запросов React Testing Library или Vitest Browser Mode

Я много писал об этом, но важно использовать правильные функции запросов (getByText(), getByPlaceholderText() и т.д.).

Использование правильных функций означает следующее:

  • тест легче читать (более понятно, что именно мы хотим проверить)

  • это способствует более реалистичному тестированию (например, поиск элементов по их подписям (labels) — это, по сути, то, что делают реальные люди при взаимодействии с формой)

  • это подталкивает к использованию семантически корректных HTML-элементов (что способствует доступности)

Например:

// ❌ Избегайте этого: `getByTestId()` - это резерв, когда больше нечего использовать
const button = screen.getByTestId(
  'submit-btn'
);
const heading = screen.getByTestId(
  'main-title'
);
const input = screen.getByTestId(
  'email-input'
);

// ✅ Хорошо - использование семантических запросов в порядке приоритета
const button = screen.getByRole(
  'button',
  { name: 'Submit' }
);
const heading = screen.getByRole(
  'heading',
  { name: 'Sign Up' }
);
const input = screen.getByLabelText(
  'Email address'
);

Порядок приоритета запросов следующий:

  1. getByRole - лучше всего подходит для кнопок, заголовков и форм

  2. getByLabelText - идеально подходит для полей ввода формы

  3. getByPlaceholderText - подходит для инпутов без подписей

  4. getByText - для неинтерактивного контента

  5. getByDisplayValue - для элементов формы со значениями

  6. getByAltText - для изображений

  7. getByTitle - для элементов с атрибутом title

  8. getByTestId - только в крайнем случае

Примечание: getByRole может быть самым медленным из всех, поэтому, несмотря на более высокий приоритет, если тесты компонентов с большим количеством элементов работают медленно, это следует учитывать.

При написании тестов самый простой способ — добавить data-testid ко всему и использовать getByTestId. Но для использования более совершенной функции запросов требуется лишь немного больше усилий.

Примечание: data-testid, безусловно, полезен. Иногда он значительно упрощает написание и поддержку тестов. Просто не злоупотребляйте им.

❯ Избегайте использования моков и отдавайте предпочтение тестированию реальных реализаций

Иногда встречаются тесты, проверяющие компонент React, в котором все дочерние компоненты имитируются (mocked).

Это связано с одним из подходов к модульному тестированию — тестированием чего-либо в полной изоляции. На мой взгляд, это устаревший способ тестирования, которого следует избегать.

Иногда на начальном этапе может быть проще так сделать (поскольку нам не важны эти компоненты. Просто создаем для них заглушки).

Я заметил, что ИИ любит чрезмерно имитировать работу компонентов, когда его просят протестировать какой-либо из них (вероятно, это связано с тем, что он не уверен, как на самом деле должны работать другие компоненты).

// ❌ Плохо - чрезмерная имитация
vi.mock('./Header', () => ({
  Header: () => (
    <div data-testid="header">
      Mocked Header
    </div>
  ),
}));

vi.mock('./Sidebar', () => ({
  Sidebar: () => (
    <div data-testid="sidebar">
      Mocked Sidebar
    </div>
  ),
}));

vi.mock('./Footer', () => ({
  Footer: () => (
    <div data-testid="footer">
      Mocked Footer
    </div>
  ),
}));

vi.mock('./UserProfile', () => ({
  UserProfile: () => (
    <div data-testid="user-profile">
      Mocked User
    </div>
  ),
}));

test('dashboard renders correctly', () => {
  render(<Dashboard />);

  expect(
    screen.getByTestId('header')
  ).toBeInTheDocument();
  expect(
    screen.getByTestId('sidebar')
  ).toBeInTheDocument();
  expect(
    screen.getByTestId('footer')
  ).toBeInTheDocument();
  expect(
    screen.getByTestId('user-profile')
  ).toBeInTheDocument();
});

Этот тест не проверяет реальную реализацию. Он проверяет наши моки. Поэтому я не вижу в этом тесте никакой пользы.

Вот улучшенная версия — она по-прежнему имитирует некоторые вещи, но на этот раз только часть, отвечающую за получение данных. Также она утверждает (assert), что каждый элемент (например, заголовок, боковая панель и т.д.) содержит ожидаемое реальное значение, а не просто data-testid с этим значением в DOM:

// ✅ Хорошо - имитируем только необходимое
vi.mock('./api/userService', () => ({
  fetchUserData: vi
    .fn()
    .mockResolvedValue({
      name: 'John Doe',
      email: 'john@example.com',
    }),
}));

test('dashboard displays user information after loading', async () => {
  render(<Dashboard />);

  // Тестируем появление данных пользователя
  expect(
    await screen.findByText(
      'Welcome, John Doe'
    )
  ).toBeInTheDocument();
  expect(
    screen.getByText('john@example.com')
  ).toBeInTheDocument();

  // Тестируем исчезновение индикатора загрузки
  expect(
    screen.queryByText('Loading...')
  ).not.toBeInTheDocument();
});

Обычно, я применяю имитацию или слежение (spyOn) для:

  • имитации ответов API

  • имитации ошибок (для проверки того, обрабатывает ли тестируемый объект такие ошибки)

  • имитации сторонней библиотеки/компонента

  • имитации кода, когда компонент использует веб-API, такие как Canvas, которые не поддерживаются React Testing Library (в идеале, эту часть компонента можно вынести в отдельный экспорт, чтобы имитировать небольшую часть компонента)

  • создания фиктивного объекта, который тестируется в другом месте и ​​не имеет отношения к тестированию данного компонента

❯ При использовании моков, старайтесь избегать их чрезмерного использования

Если вы все же решите использовать vi.mock() или jest.mock() (или их вариации, например .doMock()), имейте в виду, что вы имитируете весь модуль целиком.

Это означает, что если этот модуль начнет экспортировать новое свойство, то, вероятно, все моки потребуют обновления.

Я неоднократно сталкивался с ситуацией, когда сначала создавалась заглушка для модуля, а спустя несколько месяцев добавлялся еще один экспорт, и все тесты переставали работать.

Существует обходной путь с использованием vi.importActual() в Vitest (в Jest есть jest.requireActual()), который может помочь:

// ❌ Плохо - имитация всего модуля, сложно поддерживать
vi.mock('./userService', () => ({
  fetchUser: vi.fn().mockResolvedValue({
    name: 'John',
  }),
  updateUser: vi.fn(),
  deleteUser: vi.fn(),
}));

// ✅ Лучше - импорт настоящего модуля и имитация только необходимого
vi.mock('./userService', async () => {
  const actual = await vi.importActual(
    './userService'
  );
  return {
    // Не трогаем другие экспорты
    ...actual,
    fetchUser: vi
      .fn()
      .mockResolvedValue({
        name: 'John',
      }),
  };
});

// Версия Jest
jest.mock('./userService', () => ({
  // Не трогаем другие экспорты
  ...jest.requireActual(
    './userService'
  ),
  fetchUser: jest
    .fn()
    .mockResolvedValue({
      name: 'John',
    }),
}));

Таким образом, если модуль userService получит новые экспорты, тесты не сломаются, поскольку переопределяются только те конкретные функции, которые необходимо имитировать.

Другой (более удобный) способ решения проблемы — попытаться выделить элементы, которые будут имитироваться, в отдельные файлы, чтобы можно было «безопасно» имитировать весь файл целиком.

Но гораздо лучшим «обходным путем» является использование vi.spyOn() (или jest.spyOn()). Мы получаем автоматическую типизацию TypeScript, ее легко восстановить, и при чтении теста гораздо легче понять, что происходит:

// ✅ Хорошо - следим за конкретными методами, вместо имитации всего модуля
beforeEach(() => {
  vi.spyOn(
    userService,
    'fetchUser'
  ).mockResolvedValue({
    name: 'John',
  });
});

❯ Уделяйте приоритетное внимание исправлению нестабильных тестов

Лично я считаю нестабильные тесты почти столь же важными, как и ошибки, попавшие в продакшн.

Если приходится постоянно перезапускать тесты, потому что некоторые из них иногда проходят, а иногда нет, то:

  • во-первых, это очевидная пустая трата времени

  • нестабильность теста может быть вызвана реальной ошибкой в приложении (а не просто некорректным тестовым кодом)

  • и очень скоро инженеры понимают, что какой-то файл постоянно выдает ошибку, и просто игнорируют его, выполняя слияние (merge), даже если не все тесты CI проходят успешно. Это легко может привести к случайному слиянию другого неработающего теста

Иногда удаление нестабильного теста приносит больше пользы, чем повторный запуск теста, который может пройти успешно. Но в идеале нестабильность следует устранить.

❯ Не ограничивайтесь тестированием только оптимального сценария

“Счастливый путь” (happy path) — это самый важный путь, “успешный” или безошибочный поток (flow).

Например, “форма обратной связи успешно отправлена, ​​и данные формы отправлены в бэкэнд” — это счастливый путь (оптимальный сценарий).

Однако печальный путь (sad path) не менее важен.

Печальный путь является противоположностью счастливого. Другими словами — это когда все идет не так:

  • состояния ошибок

  • проблемы с проверкой форм

  • проблемы с сетью

  • проблемы аутентификации или авторизации

  • и крайние случаи

Все это — часть опыта, с которым столкнутся реальные пользователи.

Именно такие вещи отдел контроля качества не заметит так легко, как ошибки в счастливом пути.

// ✅ Хорошо - тестирование счастливого пути
test('submits contact form successfully', async () => {
  render(<ContactForm />);

  // ... заполняем форму
  // ... отправляем форму

  expect(
    await screen.findByText(
      'Message sent successfully!'
    )
  ).toBeVisible();
});

// ✅ Также хорошо - тестирование печального пути
test('shows error when form submission fails', async () => {
  // Имитируем API для возврата ошибки
  vi.mocked(
    fetch
  ).mockRejectedValueOnce(
    new Error('Network error')
  );

  render(<ContactForm />);

  // ... заполняем форму
  // ... отправляем форму

  expect(
    await screen.findByText(
      'Failed to send message. Please try again.'
    )
  ).toBeVisible();
});

❯ Не злоупотребляйте снимками состояния

Что такое снимки состояния (snapshots)?

Существует два типа снимков.

Первый вариант — это expect(val).toMatchSnapshot(), когда после первого запуска теста программа записывает в файл результат val. При последующих запусках теста она будет сравнивать текущее значение с этим файлом-снимком.

Второй вариант — expect(val).toMatchInlineSnapshot() очень похож, но обновляет тестовый файл и сохраняет значение непосредственно в коде.

После первого запуска Jest или Vitest заменят эту строку на что-то вроде expect(val).toMatchInlineSnapshot("some-output") (это может быть сериализованный объект, массив и т.д.).

Мне очень нравится .toMatchInlineSnapshot(). Я постоянно использую его в процессе разработки.

Даже при использовании TDD я иногда понимаю, что быстрее использовать встроенные снимки состояния, чем записывать сложные объекты в .toStrictEqual(...).

Но, на мой взгляд, они являются причиной многих проблем.

Если в кодовой базе слишком много снимков, мы привыкаем выполнять yarn test:watch с флагом u (для обновления снимков). В результате появляются ошибки, и их очень легко пропустить.

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

Но если использовать их с огромными объектами, массивами или сериализованными DOM-элементами, это быстро сделает тесты очень запутанными. Гораздо лучше проверять только те части, которые относятся к тесту.

Например, взгляните на этот чрезмерно сложный снимок:

test('updates theme to dark mode', () => {
  updateTheme(mockUserId, 'dark');

  const userProfile =
    getUserProfile(mockUserId);

  expect(userProfile)
    .toMatchInlineSnapshot(`
    {
      "id": "user123",
      "name": "John Doe",
      "email": "john@example.com",
      "preferences": {
        "theme": "dark",
        "language": "en",
        "notifications": {
          "email": true,
          "push": false,
          "sms": true,
          "marketing": false,
          "newsletter": true,
        },
        "privacy": {
          "showEmail": false,
          "showPhone": true,
          "allowAnalytics": true,
          "cookieConsent": true,
        },
      },
      "profile": {
        "bio": "Software engineer passionate about testing",
        "location": "San Francisco, CA",
        "website": "https://johndoe.dev",
        "socialMedia": {
          "twitter": "@johndoe",
          "github": "johndoe123",
          "linkedin": "john-doe-engineer",
        },
      },
      "metadata": {
        "createdAt": "2023-01-15T10:30:00Z",
        "updatedAt": "2024-03-20T14:45:30Z",
        "lastLoginAt": "2024-03-21T09:15:22Z",
        "loginCount": 247,
        "accountStatus": "active",
      },
    }
  `);
});

Когда тест начнет падать, как понять, что мы на самом деле проверяем?

Я считаю, что следующая версия гораздо чище и фокусируется только на том, что важно для данного теста:

test('updates theme to dark mode', () => {
  updateTheme(mockUserId, 'dark');

  const userProfile =
    getUserProfile(mockUserId);

  expect(
    userProfile.preferences.theme
  ).toBe('dark');
});

Вторая версия гораздо проще для чтения и понимания того, что именно проверяет тест.

Еще одно преимущество заключается в том, что если в структуру данных userProfile добавляются новые атрибуты (или удаляются старые), этот тест останется неизменным (если только не будет обновлен userProfile.preferences.theme).

Если вы и ваша команда используете снимки состояния, у меня есть два основных совета:

  • внимательно следите, чтобы разработчики, увидев, что тест не пройден, не применили флаг u (для обновления снимка) и не зафиксировали (commit) ошибку. Очень легко не заметить, что была внесена ошибка, а все, что мы сделали, это обновили тест, чтобы он прошел успешно

  • избегайте создания снимков HTML. Можно сделать что-то вроде expect(screen.getByText('hi')).toMatchInlineSnapshot(...) и получить в итоге огромные фрагменты HTML, которые будет сложно поддерживать (кроме как обновлять их с помощью u).

❯ Хорошая структура тестовых файлов с правильно названными тестами

Сочетание вложенных блоков describe(), test() (или it()) с понятными заголовками и применение .each(), где это уместно, значительно упрощают поддержку тестов.

Они позволяют вам:

  • быстро и легко находить тесты и/или определять, куда добавить новый тест, чтобы проверять связанное поведение в одном месте

  • понять, что делает каждый тест, не читая его реализацию

  • запускать определенные группы тестов (в блоке describe()) во время разработки, с помощью .only()

Когда начинаешь работать с огромными тестовыми файлами (еще один признак плохого кода) без какой-либо структуры, легко добавить дублирующийся тест или не заметить, что какой-то фрагмент кода не покрыт тестами.

❯ Хорошие тесты выполняются быстро

Нет ничего менее продуктивного, чем ждать более 30 секунд, пока выполнятся тесты.

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

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

Совет: используйте режим --watch при запуске тестов и фильтруйте результаты по названию файла. Например, vitest --watch UserProfile.test.tsx.

❯ Избегайте запросов к элементам на основе названий классов и подобных атрибутов

Распространенная ошибка — запрашивать элементы в DOM на основе таких вещей, как названия классов:

const { container } = render(
  <SomeComponent />
);
// ❌ Плохо - поиск по названиям классов
const submitButton =
  container.querySelector(
    '.submit-btn'
  );
const errorMessage =
  container.querySelector(
    '.error-text'
  );

Ситуация еще хуже, когда имена классов представляют собой автоматически сгенерированные случайные строки из CSS-in-JS.

Доступ к таким вещам, как .querySelector(), следует рассматривать как запасной вариант на тот редкий случай, когда вам действительно понадобится ими воспользоваться.

Обычно можно ограничиться стандартными запросами React Testing Library (или Vitest Browser Mode) (например getByRole() или getByText()).

Написание подобных тестов в конечном итоге усложняет поддержку и нарушает основной принцип React Testing Library, который заключается в семантическом запросе элементов.

❯ Избегайте утверждений о том, что элементы имеют конкретные названия классов

В предыдущем совете я упоминал, что не рекомендуется запрашивать элементы по имени их класса.

Верно и обратное — не следует проверять, были ли установлены конкретные классы.

Есть исключения. Я, наверное, делаю это несколько раз в год. Если вы тестируете код, связанный с тематизацией (theming), смело используйте такой подход.

Но в случае обычных тестов я всегда рассматриваю .toHaveClass() как признак плохого кода, которого следует избегать.

Без визуальных регрессионных тестов (создание реальных скриншотов и их сравнение) мы зачастую не можем доказать, что эти классы вообще что-то делают.

Встроенных средств сопоставления (matchers) так много, что обычно их более чем достаточно для достижения желаемого результата с помощью имен классов. Вот несколько примеров:

// ❌ Плохо - тестирование деталей реализации
expect(button).toHaveClass(
  'sc-bdVaJa-d'
);
expect(button).toHaveClass(
  'border-red-500'
);

// ✅ Лучше - тестирование состояний видимости и интерактивности
expect(button).toBeVisible();
expect(button).toBeEnabled();

// ✅ Хорошо - тестирование семантических атрибутов
expect(button).toHaveAttribute(
  'aria-pressed',
  'false'
);

// ✅ Хорошо - тестирование реального контента, который видят пользователи
expect(button).toHaveTextContent(
  'Submit Form'
);

// ✅ Хорошо - тестирование состояний формы
expect(input).toBeRequired();
expect(input).toHaveValue(
  'john@example.com'
);

❯ Избегайте тестирования внутренних механизмов инструментов управления состоянием (например, Redux)

При тестировании компонентов, использующих библиотеки управления состоянием, такие как Redux или Zustand, следует тестировать поведение компонента, а не внутреннюю структуру библиотеки:

// ❌ Плохо - тестирование "внутренностей" Redux
test('dispatches correct action', () => {
  const store = createMockStore();
  render(
    <Provider store={store}>
      <TodoList />
    </Provider>
  );

  const addButton = screen.getByRole(
    'button',
    { name: 'Add Todo' }
  );
  userEvent.click(addButton);

  expect(store.getActions()).toEqual([
    {
      type: 'ADD_TODO',
      payload: 'New todo',
    },
  ]);
});

// ✅ Хорошо - такой же тест,
// но в этот раз проверяется результат рендеринга,
// чтобы убедиться в добавлении нового элемента списка задач
test('adds new todo when button is clicked', () => {
  render(
    <Provider store={mockStore}>
      <TodoList />
    </Provider>
  );

  const addButton = screen.getByRole(
    'button',
    { name: 'Add Todo' }
  );
  userEvent.click(addButton);

  expect(
    screen.getByText('New todo')
  ).toBeInTheDocument();
});

Тестируйте то, с чем сталкиваются реальные пользователи, а не детали реализации. Это значительно упрощает поддержку кода и повышает ценность тестов.

❯ Избегайте тестирования с помощью .toBeDefined() — используйте более полезные методы

При проверке значения, возвращаемого функцией, может возникнуть соблазн просто добавить expect(something).toBeDefined(), чтобы убедиться, что оно не пустое.

Но зачем останавливаться на этом? В большинстве случаев, гораздо полезнее проверять конкретное значение:

// ❌ Плохо - тестирование наличия
test('user profile has data', () => {
  const user = getUserProfile();

  expect(user).toBeDefined();
  expect(user.name).toBeDefined();
  expect(user.email).toBeDefined(); // не доказывает наличия здесь email... Может быть чем угодно, включая `null`
});

// ✅ Хорошо - тестирование реальных значений и поведения
test('user profile contains correct data', () => {
  const user = getUserProfile();

  expect(user.name).toBe('John Doe');
  expect(user.email).toBe(
    'john@example.com'
  );
  expect(user.isActive).toBe(true);
});

В первом тесте проверяется, что мы возвращаем нечто, содержащие имя и адрес электронной почты. Но это не доказывает, что они имеют верные значения.

А что, если user.name — это пустая строка? Или что, если user.email содержит недопустимое значение? Тест все равно пройдет.

Кстати, во втором тесте мы даже не делаем expect(user).toBeDefined(). Это просто шум, последующие тесты все равно доказывают, что мы имеем дело с объектом.

И .toBeDefined() пройдет, даже если возвращается null:

// Эта функция ошибочно возвращает `null`
const getUserById = id => {
  // Баг: вместо данных пользователя возвращается `null`
  return null;
};

// ❌ Плохо - тест проходит, несмотря на сломанную функцию
test('getUserById returns user data', () => {
  const user = getUserById('123');

  expect(user).toBeDefined(); // Это проходит, поскольку `null` классифицируется как определенное значение (defined)
});

// ✅ Хорошо - тестируем реальную ожидаемую структуру
test('getUserById returns user with correct properties', () => {
  const user = getUserById('123');

  expect(user).toEqual({
    id: '123',
    name: 'John Doe',
    email: 'john@example.com',
  });
  // Это упадет и поймает баг
});

Тоже самое справедливо в отношении функций, возвращающих пустые массивы или пустые строки:

const getErrorMessages = () => {
  return []; // Баг - функция должна возвращать сообщения об ошибках
};

// ❌ Плохо - тест проходит, несмотря на пустой массив
test('returns error messages', () => {
  const errors = getErrorMessages();
  expect(errors).toBeDefined(); // `[]` является определенным значением
});

// ✅ Хорошо - тестируется реальный контент
test('returns validation error messages', () => {
  const errors = getErrorMessages();
  expect(errors).toEqual([
    'Email is required',
    'Password must be at least 8 characters',
  ]);
});

❯ В TypeScript: избегайте чрезмерного использования as any, будьте конкретнее

Я думаю, что в тестах мы можем быть более снисходительными к использованию некорректных типов.

Например, если нам нужно отправить в userCanCreatePost() только active: false, то можно сделать так:

// Эта функция ожидает объект `User` с кучей свойств
expect(
  userCanCreatePost({
    active: false,
  } as any)
).toBe(false);

Но гораздо безопаснее использовать утверждение реального типа, например {active: false} as User.

В этом случае, если будет опечатка, мы получим ошибку TypeScript.

❯ Используйте фикстуры и вспомогательные функции, чтобы упростить чтение и написание тестов

Если вы часто пишете похожий код для создания фиктивных данных, рассмотрите возможность использования готовых объектов с такой структурой или фикстурных (fixture) функций для их генерации:

interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

// Вспомогательная функция для создания фиктивных данных с опциональной перезаписью
export const createUser = (
  overrides?: Partial<User>
): User => {
  return {
    id: 'user-123',
    name: 'John Doe',
    email: 'john@example.com',
    isActive: true,
    preferences: {
      theme: 'light',
      notifications: true,
    },
    ...overrides,
  };
};

export const createInactiveUser =
  (): User => {
    return createUser({
      isActive: false,
    });
  };

export const createAdminUser =
  (): User => {
    return createUser({
      name: 'Admin User',
      email: 'admin@example.com',
    });
  };

test('displays user profile correctly', () => {
  const user = createUser({
    name: 'Jane Smith',
    email: 'jane@example.com',
  });

  render(<UserProfile user={user} />);

  expect(
    screen.getByText('Jane Smith')
  ).toBeInTheDocument();
  expect(
    screen.getByText('jane@example.com')
  ).toBeInTheDocument();
});

test('shows inactive status for inactive users', () => {
  const inactiveUser =
    createInactiveUser();

  render(
    <UserProfile user={inactiveUser} />
  );

  expect(
    screen.getByText('Status: Inactive')
  ).toBeInTheDocument();
});

test('shows list of users', () => {
  const users = [
    createUser({ name: 'User 1' }),
    createUser({ name: 'User 2' }),
    createInactiveUser(),
  ];

  render(<UserList users={users} />);

  expect(
    screen.getByText('User 1')
  ).toBeInTheDocument();
  expect(
    screen.getByText('User 2')
  ).toBeInTheDocument();
  expect(
    screen.getByText('Status: Inactive')
  ).toBeInTheDocument();
});

Это значительно упрощает чтение тестов. Легко понять, что если проходит createUser({isActive: false}), то важна именно часть isActive: false.

Это означает, что в случае сбоя теста становится более очевидным, что именно он пытался сделать (поскольку во вспомогательной функции будут использоваться только важные поля).

❯ Использование вспомогательных функций рендеринга для настройки общих поставщиков контекста

В большинстве реальных приложений в итоге получается такой _app.tsx или main.tsx:

return (
  <ThemeProvider currentTheme="dark">
    <ReduxProvider>
      <UserOptionsProvider>
        <FeatureFlagProvider
          enabled={query.featureSwitch}
        >
          <AuthProvider
            currentUser={user}
          >
            {props.children}
          </AuthProvider>
        </FeatureFlagProvider>
      </UserOptionsProvider>
    </ReduxProvider>
  </ThemeProvider>
);

Поэтому в тестах часто приходится вызывать метод render с указанием некоторых родительских провайдеров, поскольку они имеют решающее значение для корректной работы компонентов:

render(
  <ThemeProvider currentTheme="dark">
    <ReduxProvider>
      <FeatureFlagProvider>
        <AuthProvider
          currentUser={mockUser}
        >
          <YourActualComponentHere />
        </AuthProvider>
      </FeatureFlagProvider>
    </ReduxProvider>
  </ThemeProvider>
);

Иногда можно встретить имитацию всех этих провайдеров и их хуков useContext.

Гораздо проще иметь вспомогательную функцию, которая работают как стандартный render(), но с передачей всех родительских компонентов:

// Это может находиться в общем (shared) файле
const renderWithProviders =
  component => {
    return render(component, {
      wrapper: props => {
        return (
          <ThemeProvider currentTheme="dark">
            <ReduxProvider>
              <UserOptionsProvider>
                <FeatureFlagProvider
                  enabled={
                    query.featureSwitch
                  }
                >
                  <AuthProvider
                    currentUser={user}
                  >
                    {props.children}
                  </AuthProvider>
                </FeatureFlagProvider>
              </UserOptionsProvider>
            </ReduxProvider>
          </ThemeProvider>
        );
      },
    });
  };

// В тестах
renderWithProviders(
  <YourActualComponentHere />
);

Если вы заметили, что вам по-прежнему приходится указывать некоторые провайдеры, например, для тестирования конкретного пользователя <AuthProvider currentUser={adminUser}>...</AuthProvider> или флага функционала <FeatureFlagProvider enabled="demo">...</FeatureFlagProvider>, то просто добавьте их в качестве параметров во вспомогательную функцию:

// В отдельном файле
const renderWithProviders = (
  component,
  options
) => {
  return render(component, {
    wrapper: props => {
      return (
        <ThemeProvider
          currentTheme={
            options?.currentTheme ??
            'dark'
          }
        >
          <ReduxProvider>
            <UserOptionsProvider>
              <FeatureFlagProvider
                enabled={
                  options?.featureFlag
                }
              >
                <AuthProvider
                  currentUser={
                    options?.currentUser ??
                    user
                  }
                >
                  {props.children}
                </AuthProvider>
              </FeatureFlagProvider>
            </UserOptionsProvider>
          </ReduxProvider>
        </ThemeProvider>
      );
    },
  });
};

// В тестах
renderWithProviders(
  <YourActualComponentHere />,
  {
    featureFlag: 'demo',
    currentUser: adminUser,
  }
);

Функция renderWithProviders немного громоздкая, но ее редко приходится обновлять. Зато тесты получаются намного чище и проще для чтения/написания.

❯ Тесты должны быть такими же чистыми, как и рабочий код (с небольшим количеством дублирования при копировании и вставке)

В некоторых кодовых базах тесты рассматриваются как “дополнительный” код, без каких-либо стандартов кодирования. Они воспринимаются как нечто второстепенное.

Я считаю, что тестовые файлы должны быть почти такого же высокого качества, как и рабочий код.

Я говорю “почти”, потому что, на мой взгляд, есть некоторые исключения, например:

  • в кодовых базах TypeScript, я думаю, допустимо чаще использовать any или другие утверждения типов. Иногда это может сделать тесты более понятными (например, const mockData = { disabled: true } as SomeProduct — очевидно, что этот тест интересует только свойство disabled)

  • допускается больше копирования/вставки, если это облегчает чтение тестов. Иногда абстрагирование кода в общую функциональность в тестах нецелесообразно

  • обычно, комментарии бесполезны, если они не объясняют, для чего предназначены. Но в тестах комментарии могут быть очень полезны, указывая на то, что именно проверяет тест. Например, если в тестируемую функцию передается сложный объект, комментарий, указывающий на конкретное свойство, важное для теста, может значительно упростить его сопровождение

❯ Объясняйте ожидаемые значения

При тестировании простой функции, которая вычисляет налоговую ставку, может возникнуть соблазн сделать что-то вроде:

expect(
  calculateTaxInUsd(
    someProduct,
    country
  )
).toBe(16.4);

Но если тест упадет, как понять, что 16.4 является правильным значением?

Либо оставьте комментарий, либо рассчитайте его математически, чтобы объяснить, как оно получилось.

Без этого часто приходится просто вносить изменения в реализацию: видим, что результат изменился, и копируем/вставляем ожидаемое значение из результата неудачного теста.

❯ Не используйте реализацию для расчета ожидаемых значений

Допустим, мы тестируем компонент, который выдает значение — скажем, значение налога.

Обычно, сначала мы отрисуем компонент, а затем сделаем что-то вроде:

const expected = 16.4;
expect(
  screen.getByRole('heading')
).toHaveTextContent(
  `Tax: $${expected}`
);

Чего следует избегать, так это вызова той же функции, что и в коде приложения, для вычисления 16,4 доллара. А вдруг в этой функции есть ошибка, а мы используем ее для проверки корректности работы кода?

❯ Избегайте бесполезных тестов, не тестируйте то, что не может дать сбой

Каждый тест должен выполнять определенную функцию.

Нет смысла в тесте, который проверяет что-то несущественное, или написан таким образом, что не завершится с ошибкой, если такая появится.

Например:

test('this is a pointless test', async () => {
  const { container } = render(
    <Greeting />
  );
  expect(container).toBeDefined();
});

Провал этого теста почти невозможен (технически это возможно, если произойдет ошибка во время рендеринга).

Я также избегаю тестирования вещей, которые никогда не могут дать сбой.

Например:

const Greeting = ({ name }) => {
  return (
    <div>
      <h1>Hello, ${name}</h1>
      <h2>Welcome to the site</h2>
    </div>
  );
};

Для этого (упрощенного) компонента тестирование отображения сообщения «Hello, Fred» при рендеринге <SomeComponent name="Fred"/>, безусловно, имеет смысл.

Однако я бы никогда не стал проверять отображение «Welcome to the site». В компоненте нет логики для изменения этого текста.

Примечание: если этот компонент потенциально может быть потомком другого компонента, то проверка отображения «Welcome to the site» вполне может быть целесообразной.

Вот пример, где такая проверка уместна:

const ParentPage = ({
  isLoggedIn,
  username,
}) => {
  return (
    <div>
      {isLoggedIn ? (
        <Greeting name={username} />
      ) : (
        <JoinUp />
      )}
    </div>
  );
};

❯ Избегайте тестирования своих моков или тестирования своих тестов

Иногда легко переборщить с моками, и ваш тест просто проверяет ваш мок.

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

Признаком плохого кода в этом случае является наличие spyOn со сложным .mockImplementation().

Вот пример. Допустим, у нас есть компонент PriceCalculator, который использует taxService для расчета налогов:

// ❌ Плохо - тестирование имитации, а не реального поведения
test('displays correct price with tax', () => {
  const mockCalculateTax = vi
    .spyOn(taxService, 'calculateTax')
    .mockImplementation(
      (price, region) => {
        // Повторная реализация логики
        const taxRates = {
          US: 0.08,
          UK: 0.2,
          CA: 0.13,
        };
        return (
          price *
          (taxRates[region] || 0)
        );
      }
    );

  render(
    <PriceCalculator
      basePrice={100}
      region="US"
    />
  );

  expect(
    screen.getByText('Total: $108.00')
  ).toBeInTheDocument();
  expect(
    mockCalculateTax
  ).toHaveBeenCalledWith(100, 'US');
});

В этом примере мы повторно реализовали логику расчета налогов в моке.

На мой взгляд, лучший и более простой способ проверить это — создать заглушки для конкретных возвращаемых значений и не пытаться заново реализовывать логику в этих заглушках:

// ✅ Лучше - имитируем возвращаемые значения, тестируем поведение компонента
test('displays price with tax from tax service', () => {
  vi.spyOn(
    taxService,
    'calculateTax'
  ).mockReturnValue(8.0); // Просто мок возвращаемого значения

  render(
    <PriceCalculator
      basePrice={100}
      region="US"
    />
  );

  expect(
    screen.getByText('Total: $108.00')
  ).toBeInTheDocument();
  expect(
    taxService.calculateTax
  ).toHaveBeenCalledWith(100, 'US');
});

Не забудьте также отдельно протестировать сервис расчета налогов:

test('tax service calculates US tax correctly', () => {
  const result =
    taxService.calculateTax(100, 'US');
  expect(result).toBe(8.0);
});

Это несколько упрощенный пример, но общее правило таково: обнаружение сложной логики в моках может быть признаком того, что следует либо тестировать эту логику отдельно, либо упростить моки.

❯ Избегайте протечек между испытаниями

Каждый тест должен запускаться самостоятельно, а также в любом порядке. Запуск одних тестов не должен влиять на запуск других тестов.

Если тесты выполняются только в определенном порядке — это верный признак плохого кода.

Старайтесь избегать следующего:

  • функции-заглушки и функции слежения настраиваются в одном тесте (например, vi.spyOn(something, 'someFn').mockReturnValue(true)), а другие тесты ожидают, что эта заглушка вернет результат true для их тестов

  • базы данных наполняется данными в одном тесте, а в следующем ожидается наличие определенной строки в БД

Вы можете указать vitest запускать тесты в случайном порядке (sequence.shuffle.tests). Это снизит вероятность влияния одних тестов на другие и/или их зависимость друг от друга.

Примечание: можно использовать beforeAll()/beforeEach() для настройки параметров перед запуском теста или afterEach()/afterAll() для очистки состояния перед запуском следующего теста.

❯ Избегайте жесткого кодирования подписанных хэшей

Это похоже на жесткое кодирование «магических чисел». Но если вы когда-либо будете иметь дело с подписанными токенами (signed tokens) (например, JWT), то жесткое кодирование их значений будет крайне неудобным в плане поддержки, если только не будет ясно, как повторно сгенерировать подпись (signature) с новыми данными:

// ❌ Плохо - жесткое кодирование токена JWT без возможности повторной генерации
test('verifies valid JWT token', () => {
  const hardCodedToken =
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30';

  const decodedToken =
    verifyAndDecodeToken(
      hardCodedToken
    );
  expect(decodedToken.valid).toBe(true);
  expect(
    decodedToken.payload.name
  ).toBe('John Doe');
});

Предположим, что нам нужно добавить к токену новое свойство, например, status. То есть мы хотим добавить:

expect(result.payload.status).toBe(
  'active'
);

Но теперь восстановить исходный токен довольно сложно.

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

Примечание: токен в примере выше был сгенерирован на jwt.io.

❯ Избегайте использования операторов if/else

В тестах никогда не должно быть условных операторов (if/else) (за одним исключением, см. ниже).

Вот пример бесполезного теста, в котором из-за if(result) никогда не вызывается expect(), поэтому мы не понимаем, что у нас ошибка:

// Обратите внимание, что возвращается
// либо `undefined`,
// либо `{success: boolean}` (не `isSuccess`)
const maybeReturnsSomething = ():
  | undefined
  | { success: boolean } => {
  return undefined;
};
test('it returns isSuccess', () => {
  const result =
    maybeReturnsSomething(); // Может что-то вернуть, может ничего не вернуть
  if (result) {
    expect(result.isSuccess).toBe(true);
  }

  // Тест проходит... потому что `result` был пустым, и `expect()` не упал
});

Решением может быть простое удаление if(result). Ошибки TypeScript поможет решить оператор проверки на ненулевое значение.

test('it returns isSuccess', () => {
  const result =
    maybeReturnsSomething(); // Может что-то вернуть, может ничего не вернуть
  expect(result!.isSuccess).toBe(true); // Теперь это всегда будет запускаться (поэтому перехватит баг)
});

❯ Когда использовать условные операторы

При использовании each(), бывает полезно применять условия, но только в очень простых случаях, например:

it.each([true, false])(
  'includes button when someProp = %s',
  isEnabled => {
    render(
      <Component someProp={isEnabled} />
    );
    const maybeButton =
      screen.queryByRole('button');

    if (isEnabled) {
      expect(
        maybeButton
      ).toBeInTheDocument();
    } else {
      expect(maybeButton).toBeNull();
    }
  }
);

❯ Добавляйте комментарии или полезные названия переменных, помогающие объяснить цель теста

При чтении тестов гораздо проще понять их цель при наличии описательных имен переменных (как и в рабочем коде). Иногда дополнительные комментарии (даже если они дублируются) могут помочь понять, что именно мы тестируем или почему ожидаемый результат имеет то или иное значение.

Вот пример теста, который сложно понять:

test('handles logic', () => {
  const result = calculateCost(5);
  expect(result).toBe(9);
});

Этот тест ничего не говорит о том, что он проверяет. Что означает “handles logic”? Почему ожидаемый результат равен 9?

Вот тот же тест, переписанный с более удачными названиями переменных и комментариями:

test('calculates total cost including tax and shipping', () => {
  const baseItemCost = 5;
  const expectedTotalWithTaxAndShipping = 9; // $5 база + $2 налог + $2 доставка

  const result = calculateCost(
    baseItemCost
  );

  expect(result).toBe(
    expectedTotalWithTaxAndShipping
  );
});

Хотя это простой и надуманный пример, надеюсь, идея понятна.

Когда кто-то возвращается к этому тесту, чтобы обновить его (или исправить, если его изменения что-то сломали), становится ясно, для чего этот тест предназначен.

Примечание: этот раздел противоречит некоторым другим советам, которые рекомендуют поддерживать чистоту кода и избегать комментариев. Руководствуйтесь здравым смыслом.

❯ Проверка доступности

Для разработчика, работающего над фронтенд-приложением, важно учитывать доступность.

Но зачастую очень легко забыть об этом, проигнорировать или, что еще хуже, создать проблемы с доступностью.

Использование запросов из React Testing Library, таких как getByLabelText(), getByRole() и др., помогает убедиться в том, что наша разметка является семантически правильной.

В RTL также есть вспомогательные функции getRoles() и isInaccessible(). Честно говоря, они используются очень редко.

❯ В React Testing Library попробуйте использовать findBy вместо waitFor с getBy

Если мы используем React Testing Library и нам нужно дождаться появления чего-либо (например, после истечения таймаута или повторной отрисовки), можно попробовать использовать await waitFor(...) c expect внутри.

Для тестирования подобного асинхронного поведения наиболее «правильным» вариантом является использование функций await screen.findBy....

// ❌ Нет необходимости - waitFor + getBy
await waitFor(() => {
  expect(
    screen.getByText('Data loaded')
  ).toBeInTheDocument();
});

// ✅ Лучше - findBy ждет автоматически
expect(
  await screen.findByText('Data loaded')
).toBeInTheDocument();

Использование waitFor больше подходит для объектов, которые не отображаются в DOM, например, для проверки вызова какой-либо функции/шпиона:

const someFn = vi.fn();

await waitFor(() =>
  expect(
    window.fetch
  ).toHaveBeenCalled()
);
await waitFor(() =>
  expect(someFn).toHaveBeenCalled()
);

❯ Используйте toBeVisible() вместо toBeInTheDocument() для проверки того, что пользователи видят компоненты

Распространенная проблема при тестировании компонентов, которые скрывают или отображают элементы с помощью CSS (вместо их добавления или удаления из DOM), — это неправильное использование toBeInTheDocument() и toBeVisible().

При использовании Jest или Vitest и скрытии элементов с помощью встроенных стилей (например, style={{ display: 'none' }}) или HTML-атрибута hidden, toBeVisible() может быть очень полезным и надежным.

Но как только начинают использоваться классы (например, className="hidden" с .hidden { display: none; } в CSS-файле), Jest или Vitest не будут рассматривать элемент как скрытый, если в тестовую среду не загружена соответствующая таблица стилей.

При тестировании подобного поведения, если оно полностью основано на CSS, я бы рекомендовал использовать такие инструменты, как Playwright, Cypress или Vitest Browser Mode (но при этом необходимо убедиться, что CSS корректно загружается в тестовой среде).

❯ Имитация вызовов fetch — с помощью мока fetch или MSW

Никогда не отправляйте реальные HTTP-запросы в тестах. Всегда имитируйте вызовы API.

Лично я предпочитаю либо имитировать window.fetch, либо имитировать fetch с помощью Mock Server Worker (MSW).

Реальные вызовы API медленнее, чем заглушки, могут быть ненадежными, ненужными и сложнее для тестирования различных ситуаций (например, случаев ошибок).

Для выполнения реальных вызовов API следует отдавать предпочтение сквозному тестированию (или тестированию по контракту API).

❯ Используйте userEvent чаще fireEvent

При использовании React Testing Library, у нас есть два подхода к выполнению интерактивных действий (например, кликов по элементам или ввода текста в текстовые поля).

Предпочтительный вариант — userEvent (например, userEvent.click(somebutton)) — он гораздо реалистичнее (запускает все события, такие как движение мыши, наведение курсора, нажатие кнопки мыши и т.д.).

Также имеется набор функций fireEvent, которые могут имитировать действие (например, щелчок мышью), однако в них отсутствуют все дополнительные реалистичные действия.

По возможности старайтесь использовать userEvent. Иногда может понадобится (или придется) использовать fireEvent, но это случается довольно редко. Хороший пример, который я видел, — это тестирование сложных выпадающих списков, где userEvent старался быть слишком реалистичным и запускал onBlur таким образом, что тест становился слишком сложным.

❯ Используйте пользовательские сопоставители

Если у нас много похожих тестов, проверяющих какой-либо сложный объект, не стоит забывать, что можно писать собственные сопоставители (matchers) Jest или Vitest для утверждений expect(...).

Например, если мы тестируем пользовательские объекты во всем наборе тестов:

// Добавляем это в файл настроек Jest или Vitest
expect.extend({
  toBeValidUser(received) {
    const pass =
      received &&
      typeof received.id === 'string' &&
      typeof received.name ===
        'string' &&
      received.email.includes('@');

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be a valid user`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be a valid user`,
        pass: false,
      };
    }
  },
});

// Используем в тестах
test('creates user successfully', () => {
  const user = createUser(
    'John',
    'john@example.com'
  );

  expect(user).toBeValidUser();
});

Эти пользовательские сопоставители, настроенные с помощью expect.extend(), работают как в Jest, так и в Vitest.

❯ Не забывайте тестировать разные часовые пояса

Все, что связано с датами, представляет собой сложную задачу. А при тестировании фронтенд-приложений нельзя предполагать, что все находятся в часовом поясе UTC.

Я видел, как приложения отлично работали зимой в Великобритании (когда у нас UTC+0). Затем, летом, тесты продолжали проходить, но приложение ломалось у реальных пользователей (из-за перехода на UTC+1).

При работе с датами (особенно при вычислении разницы во времени), следует тестировать их в разных часовых поясах.

❯ Используйте фиктивные таймеры, не ждите в реальном времени

В Jest и Vitest можно использовать фиктивные таймеры. Установите системное время на определенную дату и запустите все ожидающие setTimeout или setInterval.

Тесты не должны ждать в реальном времени чего-то, что можно сымитировать и что выполняется мгновенно.

Даже если мы ждем всего 100 мс, это в сумме дает значительный результат при большом количестве тестов.

Подробнее о фиктивных таймерах в тестах Jest или Vitest можно узнать здесь.

❯ Используйте уникальные строки в фиктивных данных

Нет ничего хуже, чем увидеть ошибку, что ожидалось «mock-value», а потом поискать в коде и обнаружить 15 мест, где определяется «mock-value».

Вы значительно упростите себе жизнь, если каждое тестовое значение будет в какой-то степени уникальным, чтобы в случае сбоя было легче отследить причину ошибки:

// ❌ Плохо - общие фиктивные значения усложняют отладку
const mockUser = {
  id: 'test-id', // Такой же id, что ниже
  name: 'Test',
  email: 'test@example.com',
};

const mockProduct = {
  id: 'test-id', // Такой же id, что выше
  name: 'Test',
  price: 100,
};

Когда тест завершается ошибкой «expected ‘test-id’ but received ‘undefined’», трудно определить, какое именно фиктивное значение вызывает проблему.

// ✅ Хорошо - уникальные фиктивные значения упрощают отладку
const mockUser = {
  id: 'user-john-123',
  name: 'Bart',
  email: 'bart@example.com',
};

const mockProduct = {
  id: 'product-laptop-456',
  name: 'Macbook Pro',
  price: 2999,
};

Теперь, когда тесты завершаются неудачей из-за несоответствия id или name, мы легко сможем определить, какое фиктивное значение вызывает ошибку.


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале