Содержание:
Имитация работы с сетью через 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, но об этом в другой статье.
Примерно следующим образом выглядит схема работы приложения без MSW.
MSW использует Service Worker API
для перехвата сетевых запросов на уровне сети. Вкупе с тем, что библиотека реализована в соответствии с fetch-спецификацией это позволяет не менять работу текущих сетевых запросов. Технически это выглядит как создание нового слоя в приложении, который работает параллельно с основным функционалом. Таким образом, при использовании Mock Service Worker имитация сети полностью соответствует поведению реальных сетевых взаимодействий.
Схема образно показывающая работу приложения с интегрированным в него MSW.
«Из коробки» MSW предоставляет файл сервис-воркера, который подключается и регистрируется на этапе создания приложения. В этом же файле происходит прослушивание HTTP-запросов. Это работает по принципу условной логики: если на запрос есть мок, то он перехватывается и обрабатывается MSW, если мока нет — запрос направляется на реальный сервер, благодаря чему в проекте можно частично замокать запросы, совмещая MSW и реальный сервер.
Рабочие кейсы с использованием Mock Service Worker.
Автор рисунка: @matomi_ryu
- Параллельная разработка с backend-разработчиком. На моём личном опыте, зачастую разработка идёт таким образом, что задачу параллельно берут два разработчика: один с клиентской стороны, а второй с серверной. В таком случае прописывается контракт, по которому будет идти взаимодействие между сервером и клиентом, благодаря которому и организуются mock-запросы. Без MSW в данном случае придётся вручную писать моки в функциях, которые в дальнейшем будут переписываться на асинхронные для взаимодействия с реальным сервером.
- В случае проблем со stage-сервером. В компаниях часто было такое, что во время разработки есть всего один stage и несколько разработчиков, которые могут его «положить», а локально ставить backend не всегда является лёгкой задачей. В таком случае MSW помогал не тратить время и продолжать разработку не обращая внимания на реальный stage.
- MSW в design review. Как правило, дизайнеру без разницы, как работает сервер. Но, если во время проверки задачи дизайнер не имеет возможности взаимодействовать с тестовым окружением - задача будет висеть, пока кто-то из backend-разработчиков не исправит ошибку на stage. Включение моков на нужные запросы для stage-окружения помогает избежать такой проблемы и позволит дизайнеру проверить вёрстку. Также, MSW ещё и ускоряет процесс, т.к. frontend-разработчик может замокать не нужный для дизайнера функционал и ускорить таким образом пользовательский путь для дизайнера.
- Воспроизведение багов. Бывают случаи, когда баг известен, но у вас не получается его локализовать по каким-то причинам. На личной практике несколько раз были случаи, когда баг был многосоставной и зависел сразу от нескольких сетевых запросов на сервер и только благодаря логам ответов от сервера и мокам удавалось воспроизвести его.
Какие недостатки есть у MSW?
Автор рисунка: @matomi_ryu
- MSW не поддерживает мокирование веб-сокетов. Это значит, что если ваше приложение активно использует веб-сокеты для обмена данными в реальном времени, вам придется искать альтернативные подходы для их тестирования.
- MSW не предоставляет средств для автоматической синхронизации моковых данных с изменениями в API. При изменениях в запросах или структурах данных разработчику необходимо вручную обновлять соответствующие моки.
- MSW не поддерживает мокирование WebRTC, так как WebRTC использует прямую передачу данных между клиентами (peer-to-peer), минуя традиционные HTTP-запросы, которые MSW может перехватывать и мокировать.
Как настроить Mock Service Worker во Vue 3?
- Создаём демо-проект на Vue 3 командой в консоли. Следуем всем инструкциям и выбираем нужные нам пункты, я выбрал только TypeScript.
pnpm create vite
- Ставим необходимые библиотеки для работы mock-сервера.
pnpm install @faker-js/faker msw --save-dev
- Инициализируем service worker, который дальше будет регистрироваться в браузере. В этот файл лезть руками не понадобится. В команде указана папка
./public
потому что она не обрабатывается сборщиком, а всё её содержимое попадает в bundle проекта без изменений.
npx msw init ./public --save
- В своём проекте я использовал 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/
- Создаём функции генерации данных на
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,
});
- Пишем перехватчики запросов. Для этого мы объявляем функцию, которая принимает объект с тремя ключами:
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,
});
}
- Пишем нужные нам настройки для mock-сервера. В моём случае это всего лишь список url-адресов. Указывать возможные query-параметры запросов тут не нужно - сам MSW при перехвате запроса не обращает на них внимания.
// mocks/settings.ts
const host = "https://jsonplaceholder.typicode.com";
export const urls = {
getPosts: `${host}/posts`,
createPost: `${host}/posts`,
};
- Регистрируем перехватчики запросов. Делается это с помощью модуля
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),
];
- Инициализируем 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 самостоятельно.
- Service Worker может не работать на localhost в некоторых браузерах без SSL сертификата, поэтому возможно на этом этапе у вас не будет работать MSW в стандартном режиме, и в этом случае вам нужно обратить внимание на документацию, где есть ответ, как это исправить.
Заключение
Мой рецепт был проверен на Vue 3 в демо-проекте, использовался на работе во Vue 2 и легко адаптируется под React. Теперь вы можете, используя этот базовый рецепт и адаптировать его под свой проект.
Автор рисунка: @matomi_ryu