title


Содержание:

В прошлой статье было продемонстрировано использование Mock Service Worker (MSW) на практике с несколькими советами по его использованию в production-проектах. В этой статье мы рассмотрим, как использовать генераторы данных без MSW в тестах и как интегрировать MSW в 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),
  };
}

Обновлённый генератор использует оператор ?? (nullish coalescing, оператор нулевого слияния), который возвращает правый операнд, только если левый равен 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);
  });
});

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

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

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

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

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

© 2025 Рассудихин Алекс