Советы по использованию Mock Service Worker на production проекте


В статье я продемонстрирую практическое применение Mock Service Worker (MSW) на примере pet-проекта «Family accounting». Это SPA, которое содержит достаточное количество API-эндпоинтов, чтобы наглядно показать особенности работы MSW в приложениях с 50+ количеством запросов к серверу и дать некоторые советы по работе с генераторами данных.

Проблемы использования MSW на больших проектах

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

На проектах, где счёт API-эндпоинтов идёт на десятки или сотни, разработчики сталкиваются с несколькими серьёзными проблемами:

  • API постоянно развивается, а моки за ним не поспевают. Чем больше эндпоинтов и чем сложнее модели данных на бэкенде, тем труднее держать всё в актуальном состоянии;
  • Хаотичная организация моков быстро превращает кодовую базу в набор устаревших заглушек. Без чёткой структуры файлов и правил организации кода моки перестают соответствовать текущему состоянию API;
  • Тестирование разных сценариев становится нетривиальной задачей. Проверка поведения приложения при различных ответах сервера (404, 500, timeout) требует постоянных изменений в файлах перехватчиков запросов;
  • Добавление новой функциональности требует существенных временных затрат на создание моков, проработку структуры данных и настройку перехватчиков. При этом большая часть этой работы – рутинные операции по созданию однотипных моков и настройке стандартных перехватчиков, которые следуют одним и тем же паттернам.

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

Базовая настройка окружения

Для работы потребуется настроить Mock Service Worker в проекте. Базовая конфигурация подробно описана в предыдущей статье цикла. В дополнение к MSW будем использовать библиотеку faker.js для генерации реалистичных тестовых данных.

Организация структуры файлов проекта

Проект построен на Vue 3 с использованием модульной архитектуры. Рассмотрим организацию файловой структуры, уделив особое внимание интеграции MSW:

📂 public/
│   └── 📄 mockServiceWorker.js             # Service Worker для перехвата запросов
📂 src/
├── 📂 app/                                 # Ядро приложения
├── 📂 modules/                             # Модули приложения
├── 📂 mocks/                               # Инфраструктура моков
│   ├── 📂 generators/                      # Генераторы тестовых данных
│   │   ├── 📂 accounts/                    # Генераторы для модуля счетов
│   │   │   ├── 📄 account.ts                # Генераторы данных о счетах
│   │   │   ├── 📄 accountsList.ts           # Генератор данных для списков счетов
│   │   │   ├── 📄 accountsStatistics.ts     # Генератор данных для статистики по счетам
│   │   │   └── 📄 index.ts                  # Публичное апи генератора
│   │   ├── 📂 family/
│   │   ├── 📂 user/
│   │   ├── 📄 changelog.ts
│   │   └── 📄 pagination.ts
│   ├── 📂 handlers/            # Перехватчики запросов
│   │   ├── 📂 accounts/        # Перехватчики запросов для модуля счетов
│   │   │   ├── 📄 getAccount.ts
│   │   │   ├── 📄 getAccountsList.ts
│   │   │   ├── 📄 getAccountsStatistics.ts
│   │   │   ├── 📄 createAccount.ts
│   │   │   ├── 📄 updateAccount.ts
│   │   │   └── 📄 index.ts
│   │   ├── 📂 family/
│   │   └── 📂 user/
│   ├── 📄 index.ts             # Точка входа для моков
│   └── 📄 handlers.ts          # Регистрация перехватчиков
└── 📂 shared/                  # Общие компоненты и утилиты всего приложения

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

Советы по работе с Mock Service Worker

Совет 1: Типизация моков

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

Ключевым аспектом поддержки моков является строгая типизация данных, имитирующих серверные ответы. В нашем проекте все модели данных хранятся в директории app/types. Рассмотрим пример типизации сущности Account:

interface BaseAccountModel {
  id: string;
  createdAt: string;
  updatedAt: string;
  balance: string;
  title: string;
  description: string;
  isDeposit: boolean;
  currency: CurrencyType;
}

interface DetailAccountModel extends BaseAccountModel {
  user: UserModel;
  changelog: Changelog[];
  family: BaseFamily | null;
  history: TransactionBase[];
}

Строгая типизация предоставляет несколько важных преимуществ:

  • Автоматическое отслеживание изменений API: когда интерфейсы обновляются, TypeScript сигнализирует о несоответствиях в генераторах тестовых данных;
  • Раннее обнаружение ошибок: несоответствия между моками и реальным API выявляются на этапе компиляции;
  • Улучшенный DX: IDE предоставляет автодополнение и подсказки при работе с моками;

Даже если в определенный момент типы TypeScript отстают от изменений в API, система типов поможет постепенно привести моки в соответствие с актуальной структурой данных. Главное, чтобы сами запросы также были типизированы этими же интерфейсами. Например:

const data = await http.get<BaseAccountModel[]>('/account/list');

Совет 2: Декомпозиция генераторов данных

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

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

Ключевые идеи декомпозиции генераторов данных для MSW

  1. Атомарность: каждый генератор отвечает за создание одной конкретной структуры данных;
  2. Гибкость: генераторы принимают параметры для кастомизации создаваемых данных;
  3. Разделение ответственности: обработчики запросов (handlers) используют генераторы, но не создают данные самостоятельно, при этом сами генераторы данных отвечают только за одну схему данных.

Практическая реализация

Рассмотрим создание генератора на примере сущности «Account» (банковский счёт). Начнём с импорта необходимых зависимостей:

// mocks/generators/accounts/account.ts

import { faker } from '@faker-js/faker';

import type { BaseAccountModel, DetailAccountModel } from '@models/account';
import type { CurrencyType } from '@/app/types/main';

Создадим базовый генератор счёта. Он отвечает за генерацию основных полей сущности:

// mocks/generators/accounts/account.ts

const currencies: CurrencyType[] = ['eur', 'rub', 'tg', 'usd'];

export function generateBaseAccount(id?: string): BaseAccountModel {
  return {
    id: id || faker.database.mongodbObjectId(),
    createdAt: faker.date.between({ from: '2024-10-17', to: Date.now() }).toISOString(),
    updatedAt: faker.date.between({ from: '2024-10-17', to: Date.now() }).toISOString(),
    balance: faker.number.float({ min: 0.0, max: 3200000.0 }).toFixed(2).toString(),
    title: faker.finance.accountName(),
    description: faker.lorem.text(),
    isDeposit: faker.datatype.boolean(),
    currency: faker.helpers.arrayElement(currencies),
  };
}

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

// mocks/generators/accounts/account.ts

import { generateUser } from '../user';
import { generateChangelogList } from '../changelog';
import { generateBaseFamily } from '../family';
import { generateBaseTransactionsList } from '../transactions';

export function generateDetailAccount(id?: string): DetailAccountModel {
  return {
    ...generateBaseAccount(id),
    user: generateUser(),
    changelog: generateChangelogList(),
    family: faker.helpers.arrayElement([generateBaseFamily(), null]),
    history: generateBaseTransactionsList(),
  };
}

Такой подход к организации генераторов обеспечивает:

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

Совет 3: Создание shared-моков

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

import { faker } from '@faker-js/faker';

export function generatePagination(page: number) {
  const total = faker.number.int({ min: 25, max: 100 });

  return {
    currentPage: page + 1,
    hasNextPage: Boolean(page < Math.ceil(total / 15) - 1),
    perPage: 15,
    total,
  };
}

Другой пример – метаданные, которые сервер добавляет к большинству сущностей. Такие поля как createdAt и updatedAt встречаются практически в каждой модели. Вынесем их генерацию в отдельную функцию:

import { faker } from '@faker-js/faker';

export function generateDates() {
  return {
    createdAt: faker.date.between({ from: '2024-10-17', to: Date.now() }).toISOString(),
    updatedAt: faker.date.between({ from: '2024-10-17', to: Date.now() }).toISOString(),
  };
}

Такой подход даёт несколько преимуществ:

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

При создании shared-моков важно найти баланс – не стоит выносить в общий слой слишком специфичную логику, которая потом может измениться в разных частях приложения. Лучше ограничиться действительно общими паттернами, которые стабильны и используются во многих местах.

Заключение

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

В следующих статьях подробнее будут раскрыты темы тестирования с помощью MSW и автоматизации моков.

На изображении нарисован человек держащий в руках рамку с надписью "to be continued...", а слева от него диалоговое облачко с фразой "А что дальше?"

Автор рисунка: @matomi_ryu

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