Эмуляция бэкенда: как разрабатывать изолированный фронтенд с помощью Mock Service Worker

Сегодня я хочу рассказать о Mock Service Worker — технологии, которая позволяет эмулировать поведение бэкенда в ситуациях, когда по каким-то причинам невозможно использовать реальный бэкенд для полноценной разработки фронтенда, а также когда необходимо изолированно протестировать различные пользовательские сценарии.

Эта технология подойдёт в следующих случаях:

  • если в вашей команде фронтенд должен разрабатываться параллельно или даже раньше, чем бэкенд на основе контрактов или спецификаций;
  • если технически невозможно использовать реальный API бэкенда в условиях локальной разработки — такая ситуация возникла в моём рабочем проекте в банке, где бэкенд функционирует только в боевом контуре и доступен через виртуальную машину;
  • если вы пишите end-to-end тесты, в которых проверяете работу важных пользовательских сценариев.

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

Что такое API Mocking и при чём тут Service Workers

Mock — это модель, основанная на реальных данных, а API Mocking — это технология обработки таких моделируемых данных. Функцию API Mocking сервиса может выполнять отдельный сервер на Node.js, который запускается локально и обрабатывает запросы к API. Однако существует и другой способ обработки запросов, который не требует использования отдельного сервера, при этом отлично эмулирует сетевое поведение и предоставляет мощные инструменты для тестирования, изолированной разработки и дебага различных сетевых сценариев. Это технология Mock Service Worker (далее MSW).

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

Подробнее про Service Worker API можно почитать тут .

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

Как начать работать с Mock Service Worker

Для того чтобы настроить Mock Service Worker понадобится соответствующая библиотека msw, информацию о которой можно найти на официальном сайте: https://mswjs.io .

Установка и инициализация MSW

Чтобы установить библиотеку msw, необходимо ввести следующую команду в терминале, находясь в папке проекта:

npm install msw@latest --save-dev

Для работы с последней версией библиотеки понадобится версия Node.js не меньше 18.0.0. Если на проекте используется TypeScript, то его версию придётся поднять как минимум до версии 4.7.

Далее необходимо инициализировать Mock Service Worker и создать необходимые файлы для его работы. Это можно сделать с помощью следующей команды:

npx msw init ./public --save

Данная команда создаст файл с воркером mockServiceWorker.js в папке ./public, в которой хранятся публичные статические файлы проекта. Параметр --save позволит сохранить путь до папки в package.json проекта для будущих обновлений скрипта воркера.

Создание обработчиков запросов

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

В ранних версиях msw синтаксис работы с запросами повторял синтаксис обработчиков маршрутов на сервере Node.js, например, в Express. Однако с выпуском версии 2.0 вся логика работы была адаптирована к нативному стандарту Fetch API . Это важное изменение, позволяющее полноценно эмулировать работу с запросами с использованием современного браузерного стандарта.

Обработка запросов в MSW представлена двумя концепциями:

  1. request handler — это обработчик запросов, привязанный к конкретному URL, который перехватывает запрос и запускает функцию-резолвер;
  2. response resolver — это функция-резолвер, которая имеет доступ к параметрам запроса и возвращает ответ, эмулирующий ответ от реального бэкенда.

Давайте рассмотрим эти концепции на практике, а заодно создадим обработчик GET-запроса на эндпоинт /api/posts.

Для начала необходимо создать директорию src/mocks, в которой будет храниться всё, что относится к моковым данным. Далее создадим в ней файл handlers.js, в котором будут описаны все обработчики моковых запросов.

Начнём с импорта модуля http, который позволяет перехватывать и обрабатывать REST-запросы:

import { http } from "msw";

Следом напишем заготовку под обработчик:

// request handler
const postsHandler = http.get("/api/posts", postsResolver);

Модуль http позволяет обработать как вызов конкретного стандартного REST-метода (GET, POST, PATCH и др.), так и вызовы всех методов сразу на один и тот же URL — для этого предназначен метод модуля http.all(predicate, resolver).

Подробнее про методы модуля http можно почитать в документации msw.

Кроме REST-запросов, msw может также эмулировать запросы к GraphQL с помощью модуля graphql. Дополнительную информацию об этом можно найти в документации .

Первый параметр в request handler называется predicate (далее предикат) и отражает правила, по которым проверяется URL запроса. Предикат может быть как обычной строкой, так и регулярным выражением. Запрос будет обработан функцией-резолвером, переданной в параметре resolver, только если URL запроса совпадёт с правилом, заданным в предикате. В нашем случае предикатом является строка с относительным URL api/posts. Теперь при вызове любого GET-запроса на данный URL из клиентского приложения с работающим Mock Service Worker запустится обработчик вызова postsHandler.

Подробнее о правилах формирования предиката можно почитать в документации , а пока что пойдём дальше и напишем код функции-резолвера:

// response resolver
const postsResolver = ({ request, params, cookies }) => {
  return HttpResponse.json([
    {
      id: "0",
      title:
        "Что такое генераторы статических сайтов и почему Astro — лучший фреймворк для разработки лендингов",
      url: "https://habr.com/ru/articles/779428/",
      author: "@AlexGriss",
    },
    {
      id: "1",
      title: "Как использовать html-элемент <dialog>?",
      url: "https://habr.com/ru/articles/778542/",
      author: "@AlexGriss",
    },
  ]);
};

Response resolver предоставляет доступ к единственному аргументу в виде объекта, в котором содержится информация о перехваченном запросе.

В поле request будет доступна реализация нативного интерфейса Request из Fetch API, так что вы сможете использовать все стандартные методы и свойства соответствующего класса. В поле params будут доступны параметры пути, такие как, к примеру, postId для URL вида api/posts/:postId. Наконец, поле cookies содержит все установленные при запросе куки в виде строковых пар ключ-значение.

Функция-резолвер должна возвращать инструкцию о том, что нужно сделать при перехвате запроса. Чаще всего это будет ответ в виде mock-данных, и для этого в msw используется инстанс нативного класса Response из всё того же Fetch API. Вы можете использовать класс Response напрямую, но разработчики msw рекомендуют работать с библиотечным классом HttpResponse, который является более продвинутой надстройкой над Response. Например, он позволяет мокать установку cookies и дополняет стандартные методы класса Response удобными методами для отправки ответа с различными Content-Type.

О классе HttpResponse можно почитать подробнее в документации библиотеки msw.

Кроме обычных текстовых данных в виде plain text, json, xml или formData, библиотека msw позволяет возвращать стримы ReadableStream. Подробнее об этом можно почитать в соответствующем разделе документации .

В приведённом примере вызывается метод HttpResponse.json(), в который передаётся моковая структура данных — в данном случае это коллекция постов на Хабре. Далее она будет отправлена на клиент при успешной обработке вызова.

Давайте посмотрим на то, как должен выглядеть итоговый файл handlers.js:

// src/mocks/handlers.js

import { HttpResponse, http } from "msw";

// response resolver
const postsResolver = () => {
  return HttpResponse.json([
    {
      title:
        "Что такое генераторы статических сайтов и почему Astro — лучший фреймворк для разработки лендингов",
      url: "https://habr.com/ru/articles/779428/",
      author: "@AlexGriss",
    },
    {
      title: "Как использовать html-элемент <dialog>?",
      url: "https://habr.com/ru/articles/778542/",
      author: "@AlexGriss",
    },
  ]);
};

// request resolver
const postsHandler = http.get("/api/posts", postsResolver);

export const handlers = [postsHandler];

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

Настройка Mock Service Worker для работы в браузере

Далее в папке src/mocks создадим файл browser.js, в котором нужно будет настроить работу воркера с ранее добавленными обработчиками вызовов:

// src/mocks/browser.js

import { setupWorker } from "msw/browser";

import { handlers } from "./handlers";

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

Функция setupWorker принимает список обработчиков и подготавливает канал связи между клиентом и воркером mockServiceWorker.js, который мы сгенерировали на первом этапе.

Далее мы готовы активировать работу MSW через вызов метода воркера worker.start(). Ниже показан пример активации воркера во входной точке React-приложения:

// src/index.jsx

import React from "react";
import ReactDOM from "react-dom";

import { App } from "./App";

async function enableMocking() {
  if (process.env.NODE_ENV === "development") {
    const { worker } = await import("./mocks/browser");

    return worker.start();
  }
}

const rootElement = ReactDOM.createRoot(document.getElementById("root"));

enableMocking().then(() => {
  rootElement.render(<App />);
});

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

Для приложения на ванильном JS достаточно просто запустить метод worker.start() на этапе инициализации приложения.

Если всё сделано правильно, то при запуске приложения в консоли браузера можно будет увидеть сообщение:

[MSW] Mocking enabled.

MSW умеет работать не только в браузерной среде, но и в связке с Node.js и React Native. Настроить моки для данных окружений помогут соответствующие разделы в документации библиотеки: https://mswjs.io/docs/integrations/node и https://mswjs.io/docs/integrations/react-native .

Проверка обработки запросов

Теперь если в клиентском приложении сделать запрос на URL /api/posts, Mock Service Worker перехватит этот запрос и вернёт соответствующие ему mock-данные.

Давайте рассмотрим это на примере компонента Posts в рамках React-приложения:

// src/components/Posts.jsx

import { useEffect, useState } from "react";

import { Post } from "./Post";

export const Posts = () => {
  const [posts, setPosts] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/posts`)
      .then((response) => response.json())
      .then((posts) => {
        setPosts(posts);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, []);

  return isLoading
    ? "Loading..."
    : posts.map((post) => <Post key={post.title} post={post} />);
};

При загрузке React-компонента на вкладке Network в Developer Tools браузера отобразится XHR-запрос на URL api/posts с ответом в виде mock-данных:

Запрос на /api/posts
Запрос на /api/posts

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

В заключение

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

MSW умеет эмулировать работу как с обычными REST-запросами, так и с GraphQL и даже возвращать стримы в качестве ответа на запрос. Библиотека msw позволяет настроить интеграцию сервис-воркера с различными средами, такими как браузер, Node.js и React Native. Эмуляция реального сетевого поведения стала ещё более доступной в новой версии MSW с введением интерфейсов обработчиков запросов, которые повторяют реализацию нативных интерфейсов Fetch API, таких как Request и Response.

Мы изучили основы работы, подключения и настройки технологии Mock Service Worker, но на этом её возможности не заканчиваются. Предлагаю попробовать поэкспериментировать с MSW самостоятельно, повторив шаги из руководства, представленного в этой статье. Продолжить изучение технологии можно на сайте с документацией к библиотеке msw.


Приглашаю вас подписаться на мой телеграм-канал: https://t.me/alexgriss , в котором я пишу о фронтенд-разработке, публикую полезные материалы, делюсь своим профессиональным мнением и рассматриваю темы, важные для карьеры разработчика.