Как использовать Mock Service Worker в тестах?


Содержание:

В прошлой статье было продемонстрировано использование Mock Service Worker (MSW) на практике с несколькими советами по его использованию в production-проектах. В этой статье мы рассмотрим, как использовать генераторы данных без MSW в тестах и как интегрировать service worker в unit и e2e-тесты. В статье будет использоваться код из прошлого урока, поэтому рекомендую ознакомиться с ней.

Используем генераторы данных без MSW в тестах

Помимо работы с Mock Service Worker, созданные генераторы можно применять и в unit-тестах. Однако для этого требуется добавить возможность контроля над генерируемыми данными т.к. в тестах нам важно контролировать данные, которые мы передаём в тестируемую функцию. Это необходимо для проверки базового функционала, граничных случаев и корректности обработки ошибок.

Доработка генераторов данных для unit-тестов

Доработаем наши генераторы, чтобы сделать их более гибкими. Для этого создадим интерфейс с настройками генерации:

type PartialBaseAccount = Partial<Omit<BaseAccountModel, 'id'>>;

export interface DetailAccountArgs {
  baseAccount?: PartialBaseAccount;
  withFamily?: boolean;
  changelogLimit?: number;
}

Кратко про параметры интерфейса:

  • baseAccount использует утилитарные типы TypeScript:
    • Partial<T> делает все поля типа необязательными, что позволяет передавать только те данные, которые нужны для конкретного теста. Остальные поля будут сгенерированы через faker.js;
    • Omit<T, K> исключает указанные свойства из типа. В нашем случае удаляем id, который передаётся отдельным аргументом функции. Альтернативой было бы объединение всех параметров в один объект настроек, но это потребовало бы рефакторинга существующего кода;
  • withFamily позволяет явно указать, должна ли модель счёта содержать данные о семье;
  • changelogLimit задаёт количество записей в истории изменений счёта.

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

export function generateBaseAccount(id?: string, options: PartialBaseAccount = {}): BaseAccountModel {
  const now = Date.now();
  const dateParams = { from: '2024-10-17', to: now };
  const balanceParams = { min: 0.0, max: 3200000.0 };

  return {
    id: id ?? faker.database.mongodbObjectId(),
    createdAt: options.createdAt ?? faker.date.between(dateParams).toISOString(),
    updatedAt: options.updatedAt ?? faker.date.between(dateParams).toISOString(),
    balance: options.balance ?? faker.number.float(balanceParams).toFixed(2).toString(),
    title: options.title ?? faker.finance.accountName(),
    description: options.description ?? faker.lorem.text(),
    isDeposit: options.isDeposit ?? faker.datatype.boolean(),
    currency: options.currency ?? faker.helpers.arrayElement(currencies),
  };
}

Обновлённый генератор использует оператор ?? (оператор нулевого слияния), который возвращает правый операнд, только если левый равен null или undefined. Это позволяет передавать в options даже «ложные» значения вроде пустой строки или false.

Аналогично модифицируем генератор детальной информации о счёте:

const generateFamilyForAccount = (withFamily: boolean) =>  withFamily ? generateBaseFamily() : null;

export function generateDetailAccount(id?: string, options: DetailAccountArgs = {}): DetailAccountModel {
  return {
    ...generateBaseAccount(id, options.baseAccount),
    user: generateUser(),
    changelog: 'changelogLimit' in options ? generateChangelogList(options.changelogLimit) : generateChangelogList(),
    family: 'withFamily' in options
      ? generateFamilyForAccount(options.withFamily!)
      : faker.helpers.arrayElement([generateBaseFamily(), null]),
    history: generateBaseTransactionsList(),
  };
}

В расширенном генераторе особое внимание стоит обратить на поле family. Если флаг withFamily не указан в опциях, используется случайный выбор через faker.js. Если же флаг передан, то семья либо гарантированно будет добавлена в модель счёта, либо гарантированно будет отсутствовать – это важно для тестирования различных состояний приложения.

Пишем тесты с использованием этих генераторов

На примере тестирования страницы счёта рассмотрим, как использовать наши генераторы на практике. На этой странице есть заголовок, который отображает тип счёта в одном из четырёх вариантов: «Личный депозит», «Личный счёт», «Семейный депозит» или «Семейный счёт».

По частям разберём написанные мною тесты:

Для начала, создаём тип данных для перечисляемого значения возможных кейсов, чтобы потом использовать метод forEach в тестах.

interface AccountTypeTest { 
  name: string;                 // Описание теста
  config: {                     // Конфигурация генератора
    baseAccount: { 
      isDeposit: boolean;       // Депозит или обычный счёт
    };
    withFamily: boolean;        // Семейный или личный счёт
  };
  expectedTitle: string;        // Ожидаемый заголовок
}

Затем на основе этого типа мы создаём набор тестовых сценариев под каждый из четырёх возможных заголовков:

const accountTypeTests: AccountTypeTest[] = [
  {
    name: 'семейный счёт',
    config: {
      baseAccount: { isDeposit: false },
      withFamily: true
    },
    expectedTitle: 'Семейный счёт'
  },
  // ... остальные сценарии для разных типов счетов
];

И наконец пишем тесты с использованием цикла:

accountTypeTests.forEach(({ name, config, expectedTitle }) => {
  it(`отображает ${name}`, async () => {
    const mockAccount = generateDetailAccount(1, config);

    vi.mocked(AccountsRepository.getAccountDetails).mockResolvedValue(mockAccount);

    const wrapper = mount(AccountPage);
    await flushPromises();

    expect(wrapper.get(`[data-test-id="${testId}"]`).text()).toBe(expectedTitle);
  });
});

При таком подходе к unit-тестированию приложения мы имеем то, что:

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

Как использовать Mock Service Worker в Unit-тестах?

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

Как интегрировать MSW в Vitest?

Для того, чтобы использовать MSW в unit-тестах, необходимо создать экземпляр setupServer и запустить его в beforeAll. Пример показан на основе моего pet-проекта “Family Accounting”.

import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { handlers } from '@/mocks/handlers';

const server = setupServer(...handlers);

describe('component', () => {
  // Запускаем перехватчик запросов перед всеми тестам
  beforeAll(() => {
    server.listen();
  });

  // Останавливает перехватчик запросов после всех тестов
  afterAll(() => {
    server.close();
  });

  // Сбрасываем перехватчики запросов после каждого теста
  afterEach(() => {
    server.resetHandlers();
  });

  it('должен загрузить список аккаунтов', async () => {
    const { loading, accountsList, pagination, fetchAccounts } = useAccountsList('user');

    await fetchAccounts();

    expect(loading.value).toBe(false);
    expect(accountsList.value.length).toBe(15);
    expect(pagination.value.currentPage).toBe(2);
  });
});

Минусы такого подхода:

  • Магические числа. В примере кода присутствует жесткое связывание теста с данными из мока — 15 элементов и 2 страницы. Тесты скрыто зависят от имплементации моков, которые даже не импортируются напрямую в тестовый файл. При изменении этих значений в mock-функции тест неизбежно упадет.
  • Чрезмерная сложность. Настройка MSW в тесте усложняет его понимание и замедляет выполнение, в то время как тесты должны оставаться простыми и понятными.
  • Сложность отладки. Тест может упасть по множеству непрозрачных причин: изменения в перехватчике запросов, модификации генератора данных, правки в глобальном файле настройки перехватчиков, изменения в API MSW после обновления библиотеки.

P.S. Весь код установки MSW можно вынести в отдельный файл vitest-setup.ts и подключить в vitest.config.ts через опцию setupFiles. Это избавит от дублирования кода, но активирует MSW во всех тестах, даже там, где он не нужен. Использовать такой подход не рекомендую.

Как интегрировать MSW в Jest?

Для использования внутри конкретного теста синтаксис практически идентичен Vitest — различий в API нет. Основное отличие появляется при глобальной настройке: в конфигурации Jest (jest.config.js) используется опция setupFilesAfterEnv вместо setupFiles, которую мы видели в Vitest.

Как использовать Mock Service Worker в Playwright?

Как тестировать гонки запросов с помощью MSW?

Преимущества и недостатки MSW в e2e-тестировании

Преимущества:

  • Скорость выполнения. MSW исключает обращения к реальному API, что значительно ускоряет прогон тестов;
  • Предсказуемость. MSW обеспечивает контроль над получаемыми данными, гарантируя стабильность тестов. При работе с тестовым сервером существует риск падения тестов из-за изменения данных;
  • Гибкость. Библиотека позволяет настраивать моки запросов для проверки граничных случаев и корректности обработки ошибок.
  • Независимость от бэкенда. Тестирование фронтенда может проводиться до полной готовности API, что ускоряет разработку.

Недостатки:

  • Дублирование функционала. Playwright уже содержит встроенные инструменты для перехвата и подмены запросов. Использование MSW оправдано в основном для приложений с SSR, где Playwright не может подменить серверные запросы, поскольку они выполняются до загрузки страницы в браузере и вне контекста тестовой среды;
  • Усложнение инфраструктуры. Интеграция MSW добавляет дополнительный слой сложности в процесс e2e-тестирования, что усложняет поддержку тестов.

Когда стоит использовать MSW в e2e-тестах

MSW в e2e-тестировании будет полезен в следующих случаях:

  1. SSR-приложения — когда запросы выполняются на сервере до рендеринга страницы, а их нужно подменять на моки;
  2. Тестирование граничных случаев — для симуляции редких ситуаций и ошибок API без обращения к реальному серверу;
  3. Нестабильный или недоступный API — когда тестовый сервер может быть недоступен или обладает нестабильным набором тестовых данных;
  4. Разработка в параллель с бэкендом — когда API находится в разработке, но нужно тестировать интерфейс уже сейчас;
  5. Воспроизведение сложных сценариев работы с API — когда требуется протестировать последовательность зависимых запросов.
© 2025 Рассудихин Алекс