На рисунке изображён человек держащий в руках коробку с надписью "Request" и несущий её к символически представленному Mock Service Worker-у в виде человека, который решает в какую из дверей зайдёт человек с коробкой. Две двери по бокам от контролёра символизируют backend и моки с соотетствующими надписями на дверях

Имитация работы с сетью через Mock Service Worker


Последнее обновление:

В этой статье я расскажу о том, что такое Mock Service Worker и для чего его использовать. На личном опыте расскажу, как MSW сэкономит время и нервы разработчика. В конце статьи сделаю шаблон, как настроить Mock Service Worker во Vue 3 приложении с использованием библиотеки faker.js. Пример интеграции, который я напишу актуален для любых SPA-приложений.

P.S.: Про использование MSW в тестировании выйдет отдельная статья.

Что такое Mock Service Worker?

Mock Service Worker (MSW) — это библиотека для создания и управления моками API запросов в браузере и Node.js приложениях. MSW написан так, что не зависит от технологического стека проекта и подходит для работы с любыми JavaScript-фреймворками, такими как React, Vue, Svelte, Astro.js и другими. MSW разработан таким образом, чтобы не зависеть от библиотек, используемых для работы с сетью (например, axios), при условии, если эти библиотеки fetch-совместимы. Разработчики библиотеки придерживались спецификации fetch, что означает полное соответствие запросов к серверу и ответов с теми, которые мы получали бы при взаимодействии с реальным backend-ом. К слову, MSW поддерживает и GraphQL, но об этом в другой статье.

Схема взаимодействия: FRONTEND обменивается данными с API, API подключен к BACKEND для передачи и получения данных.

Примерно следующим образом выглядит схема работы приложения без MSW.

MSW использует Service Worker API для перехвата сетевых запросов на уровне сети. Вкупе с тем, что библиотека реализована в соответствии с fetch-спецификацией это позволяет не менять работу текущих сетевых запросов. Технически это выглядит как создание нового слоя в приложении, который работает параллельно с основным функционалом. Таким образом, при использовании Mock Service Worker имитация сети полностью соответствует поведению реальных сетевых взаимодействий.

Схема взаимодействия компонентов: FRONTEND обменивается данными с API, API подключен к SERVICE WORKER, который перенаправляет запросы на BACKEND или МОКИ.

Схема образно показывающая работу приложения с интегрированным в него MSW.

«Из коробки» MSW предоставляет файл сервис-воркера, который подключается и регистрируется на этапе создания приложения. В этом же файле происходит прослушивание HTTP-запросов. Это работает по принципу условной логики: если на запрос есть мок, то он перехватывается и обрабатывается MSW, если мока нет — запрос направляется на реальный сервер, благодаря чему в проекте можно частично замокать запросы, совмещая MSW и реальный сервер.

Рабочие кейсы с использованием Mock Service Worker.

На изображение в самом центре изображён мускулистый человек с надписью "MSW" на футболке.

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

  1. Параллельная разработка с backend-разработчиком. На моём личном опыте, зачастую разработка идёт таким образом, что задачу параллельно берут два разработчика: один с клиентской стороны, а второй с серверной. В таком случае прописывается контракт, по которому будет идти взаимодействие между сервером и клиентом, благодаря которому и организуются mock-запросы. Без MSW в данном случае придётся вручную писать моки в функциях, которые в дальнейшем будут переписываться на асинхронные для взаимодействия с реальным сервером.
  2. В случае проблем со stage-сервером. В компаниях часто было такое, что во время разработки есть всего один stage и несколько разработчиков, которые могут его «положить», а локально ставить backend не всегда является лёгкой задачей. В таком случае MSW помогал не тратить время и продолжать разработку не обращая внимания на реальный stage.
  3. MSW в design review. Как правило, дизайнеру без разницы, как работает сервер. Но, если во время проверки задачи дизайнер не имеет возможности взаимодействовать с тестовым окружением - задача будет висеть, пока кто-то из backend-разработчиков не исправит ошибку на stage. Включение моков на нужные запросы для stage-окружения помогает избежать такой проблемы и позволит дизайнеру проверить вёрстку. Также, MSW ещё и ускоряет процесс, т.к. frontend-разработчик может замокать не нужный для дизайнера функционал и ускорить таким образом пользовательский путь для дизайнера.
  4. Воспроизведение багов. Бывают случаи, когда баг известен, но у вас не получается его локализовать по каким-то причинам. На личной практике несколько раз были случаи, когда баг был многосоставной и зависел сразу от нескольких сетевых запросов на сервер и только благодаря логам ответов от сервера и мокам удавалось воспроизвести его.

Какие недостатки есть у MSW?

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

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

  1. MSW не поддерживает мокирование веб-сокетов. Это значит, что если ваше приложение активно использует веб-сокеты для обмена данными в реальном времени, вам придется искать альтернативные подходы для их тестирования.
  2. MSW не предоставляет средств для автоматической синхронизации моковых данных с изменениями в API. При изменениях в запросах или структурах данных разработчику необходимо вручную обновлять соответствующие моки.
  3. MSW не поддерживает мокирование WebRTC, так как WebRTC использует прямую передачу данных между клиентами (peer-to-peer), минуя традиционные HTTP-запросы, которые MSW может перехватывать и мокировать.

Как настроить Mock Service Worker во Vue 3?

  1. Создаём демо-проект на Vue 3 командой в консоли. Следуем всем инструкциям и выбираем нужные нам пункты, я выбрал только TypeScript.
pnpm create vite
  1. Ставим необходимые библиотеки для работы mock-сервера.
pnpm install @faker-js/faker msw --save-dev
  1. Инициализируем service worker, который дальше будет регистрироваться в браузере. В этот файл лезть руками не понадобится. В команде указана папка ./public потому что она не обрабатывается сборщиком, а всё её содержимое попадает в bundle проекта без изменений.
npx msw init ./public --save
  1. В своём проекте я использовал JSON Placeholder и настраивать моки я буду под него. Файловая структура моего проекта с учётом моков:
📂 public/
│   └── 📄 mockServiceWorker.js
📂 src/
├── 📂 components/
├── 📂 pages/
├── 📂 mocks/
│   ├── 📂 data/
│   │   ├── 📄 createPost.ts
│   │   └── 📄 getPosts.ts
│   ├── 📂 handlers/
│   │   ├── 📄 createPost.ts
│   │   ├── 📄 getPosts.ts
│   │   └── 📄 index.ts
│   ├── 📄 index.ts
│   └── 📄 settings.ts
└── 📂 utils/
  1. Создаём функции генерации данных на faker.js.

В запросах, где нет особых требований к конечным данным из моков, можно использовать faker.js для их генерации. В примере также есть mock-функция, которая имитирует получение userId из localStorage. В production вместо localStorage могут быть query-параметры, cookie или state manager (Pinia, Vuex и другие).

// mocks/data/getPosts.ts

export const getPosts = (limit: number = 15, userId?: number): IPost[] => {
  const mockData = [];

  for (let i = 1; i <= limit; i++) {
    mockData.push({
      userId: userId ?? faker.number.int({ min: 1, max: 100 }),
      id: i,
      title: faker.lorem.words(),
      body: faker.lorem.paragraph(),
    });
  }

  return mockData;
};
// mocks/data/createPost.ts

import type { IPost } from "@/types/Posts";

interface IRequestBody {
  title: string;
  body: string;
  userId: number;
}

export const createPost = (body: IRequestBody): IPost => ({
  title: body.title,
  body: body.body,
  userId: body.userId,
  id: 101,
});
  1. Пишем перехватчики запросов. Для этого мы объявляем функцию, которая принимает объект с тремя ключами: request, params, cookie. В request лежит вся информация о запросе: его метод, полный url, заголовки запроса и тело запроса. params и cookie в дополнительном уточнении не нуждаются.
// mocks/handlers/getPosts.ts

import { HttpResponse } from "msw";
import { getPosts } from "../data/getPosts";

const getUserId = (): number | null => {
  return localStorage.getItem("userId")
    ? Number(localStorage.getItem("userId"))
    : null;
};

export function getPostsHandler({ request }) {
  const searchParams = new URLSearchParams(new URL(request.url).search);
  const limit = Number(searchParams.get("limit"));

  return HttpResponse.json(getPosts(limit, getUserId()), {
    status: 200,
  });
}

На этом примере я показал, как можно использовать body самого запроса для генерации нужного mock-ответа.

// mocks/handlers/createPost.ts

import { HttpResponse } from "msw";
import { createPost } from "../data/createPost";

export async function createPostHandler({ request }) {
  const requestBody = await request.json();

  return HttpResponse.json(createPost(requestBody), {
    status: 200,
  });
}
  1. Пишем нужные нам настройки для mock-сервера. В моём случае это всего лишь список url-адресов. Указывать возможные query-параметры запросов тут не нужно - сам MSW при перехвате запроса не обращает на них внимания.
// mocks/settings.ts

const host = "https://jsonplaceholder.typicode.com";

export const urls = {
  getPosts: `${host}/posts`,
  createPost: `${host}/posts`,
};
  1. Регистрируем перехватчики запросов. Делается это с помощью модуля http из пакета msw. В этом модуле содержатся функции с теми же именами, как в методах запросов: get, post, put, delete, options, patch. Каждая из этих функций принимает всего два аргумента: абсолютный url мокируемого запроса и функция перехватчик для этого запроса.
// mocks/handlers/index.ts

import { http } from "msw";
import { urls } from "@/mocks/settings";

import { createPostHandler } from "./createPost";
import { getPostsHandler } from "./getPosts";

export const handlers = [
  http.get(urls.getPosts, getPostsHandler),
  http.post(urls.createPost, createPostHandler),
];
  1. Инициализируем service worker в проекте.
// mocks/index.ts

import { setupWorker } from "msw/browser";
import { handlers } from "@/mocks/handlers";

export const worker = setupWorker(...handlers);

В самом main-файле приложения документация MSW рекомендует использовать promise для управления включением service worker-а.

// main.ts

import { createApp } from "vue";
import App from "./App.vue";

const prepare = async () => {
  if (!isProduction()) {
    const { worker } = await import("./mocks");
    return worker.start();
  }

  return;
};

const app = createApp(App);

prepare().then(() => {
  app.mount("#app");
});

В примере для включения MSW используется лишь один флаг - isProduction. В самом же проекте можно создать дополнительный компонент с кнопкой включения и показывать этот компонент только на тестовых окружениях, чтобы любые разработчики могли включать и отключать mock service worker самостоятельно.

  1. Service Worker может не работать на localhost в некоторых браузерах без SSL сертификата, поэтому возможно на этом этапе у вас не будет работать MSW в стандартном режиме, и в этом случае вам нужно обратить внимание на документацию, где есть ответ, как это исправить.

Заключение

Мой рецепт был проверен на Vue 3 в демо-проекте, использовался на работе во Vue 2 и легко адаптируется под React. Теперь вы можете, используя этот базовый рецепт и адаптировать его под свой проект.

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

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

Ссылки

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