Итак, прошел год с предыдущей серии, многое поменялось, из каждого утюга сообщают, что вот-вот нейронки заменят всех и вся, а я всё также тружусь во fuse8 и пишу тесты для vue-компонентов.

В этой серии мы поговорим интеграции с mock service worker (msw). Так же опишу, что пытался внедрить в борьбе за живучесть, что из этого получилось, а что — не очень.

Ну что, помогли тебе твои тесты?

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

Вот основные места, где тесты хорошо себя показали:
- потеря/изменения контрактов;
- исправление последствий конфликтов (ввиду специфики наших процессов, это самый частый кейс );
- рефакторинг (объективно сложно оценить, так как покрытие проекта не большое, но перед рефакторингом стараюсь покрыть код хотя бы локальными тестами).

Опять-таки всякие курсоры с навороченным автокомплитом высвободили время, почему бы не потратить его на тесты?

Борьба за надежность, или реальность наносит ответный удар.

Начну с того, что не получилось.

Итак, первое, что я пытался сделать — это фича E2E-тесты с playwright. То есть, тестировать работу бизнес-логики в браузере, повторяя действия пользователей. 

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

В теории всё просто: берем базу, подгоняем данные, создаем докер образ → профит. Собственно, я встал на первом же этапе подготовки базы. Для этого, нужно волевое решение и комплексный подход в виде помощи бекенда и девопсов (они всегда заняты). В итоге, на моем проекте идею оставили до лучших времен.

Пробовал заменить полноценную работу с бекендом моками API-запросов в Playwright, но это оказался скорее тупиковый путь: поддержка еще одних моков (msw уже был) + долгий запуск в браузере — это нерационально, или для очень специфических задач.

Хайпанем немножечко на нейронках

Полностью от Playwright я не отказался и активно использую его в локальных тестах, которые не пойдут в продакшен (их можно делать в папках, добавленных в .gitignore).

Из последнего — мне надо было обновить UI-библиотеку Element Plus на 3 мажорные версии. Давайте откровенно: в breaking changes буквально  пара слов о том, как изменился функционал, а вот как поменялись всякие css переменные — большой вопрос. Нужно исследовать…

Так что…Нейронка, настало твое время! Скормив курсору route.js со всеми урлами приложения, получил файл routes.spec.js, в котором генерировались скриншоты всех страниц.

Затем, накатывая очередную мажорную версию библиотеки, просто запускал сравнение текущего вида с эталонным и получал вот таких красавчиков (напомню, что тут изображено смещение элементов относительно эталонного изображения):

Различия между текущим видом и эталонным
Различия между текущим видом и эталонным

Правим и накатываем следующую версию. Таким образом, миграция прошла, как мне показалось, без сюрпризов. Точнее, все места были подсвечены и поправлены.

Кажется, это тот кейс, в котором нейронка сильно сократила написание однотипного кода. Да и скриншоты тут очень хорошо подошли (в продакшене из-за своей хрупкости скриншоты у меня не прижились).

Наконец то про unit-тесты и msw

В общем и целом, я сконцентрировался на юнитах (они же интеграционные в классическом понимании). Они быстрые, изолированные, простые и надежные.

Для мокирования работы с сетью подключил mws (mock service worker), что в дальнейшем позволило практиковать контрактное программирование и параллельную разработку.

Итак, сперва устанавливаем MSW (тут вам в помощь официальный гайд).

Затем выносим конфигурацию для vite в vitest.workspace.js (в новых версия DEPRECATED, vitest еще не обновлял). Это не обязательно, но удобно, если нужно делить на окружение ноды и браузера.

import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  'packages/*',
  {
    extends: './vite.config.js',
    test: {
      environment: 'jsdom',
      name: 'unit',
      include: ['src/**/*.spec.{ts,js}'],
      deps: {
        inline: ['element-plus'],
      },
      setupFiles: ['./src/mocks/setup.ts'], // путь для конфига msw
    },
  },
]);

Так как это независимый сервис, поместил в папку mocks, чтобы в случае необходимости выпиливание было простым.

структура
структура
import { server } from './server.ts';
import { afterAll, afterEach, beforeAll } from 'vitest';

beforeAll(() =>  return server.listen({ onUnhandledRequest: 'warn' });
afterAll(() => server.close());
afterEach(() => server.resetHandlers());

user/handlers.ts

import { GET_USERS } from '@/api/constants/APIEndPoints.js';
import { HttpResponse, http } from 'msw';
import { USERS_FAKE_RESPONSE} from './fixtures.ts';

export const handlers = [
  http.get('*' + GET_USERS , () => {
    return HttpResponse.json(USERS_FAKE_RESPONSE);
  }),

];

Теперь при обращении к урлу, хранящемся в идентификаторе GET_USER, будет возвращаться значение, которое хранится в USER_FAKE_RESPONSE

Примечательно, что msw, обмазанный плагинами, позволяет сгенерировать перехватчики из openApi.json, что может покрыть все запросы к API, а также с помощью faker.js сгенерировать ответ с данными.

Мне такой вариант не нравится (затрудняет параллельную работу), поэтому я предпочитаю создавать фикстуры ответов и перехватчики ручками, ну и заполнять нейронками — получается более человекочитаемый ответ:

export const USER_FAKE_RESPONSE = {
  items:[
    { firstName: 'Иван' , lastName: 'Иванов'}
    { firstName: 'Петр' , lastName: 'Сидоров'}
  ]
}

Использование в тестах

Для наглядного примера представим, что у нас есть компонент, состоящий из кнопки получения пользователей и блока с отображением полученного ответа. Тогда тест по старинке может выглядеть так (подробный тест был в предыдущей серии тут все схематично)

import * as USER_API from 'some api folder'
let wrapper

const createComponent = (params {}) => {
  wrapper = shallowMount(OurGetUsersComponent, {
    props: {
      ...params.props,
    },
    global: {
      renderStubDefaultSlot: true,
      stubs: {
        ...params.stubs,
      },
    },
  });
};

test('Обработка получения пользователей при нажатии на кнопку Найти', async () => {
const spyGetUsers =  vi.spyOn(USER_API, 'getUsersRequest').mockImplementation(() =>{  items:[
    { firstName: 'Иван' , lastName: 'Иванов'}
    { firstName: 'Петр' , lastName: 'Сидоров'}
  ]}) 

  createComponent ()
  const buttonNode = wrapper.find('.button') //не очень удачный селектор, но у нас 1 кнопка
  await buttonNode.trigger('click');

  await flushPromises();
  expect(spyGetUsers).toHaveBeenCalled(); //тут же можно проверить параметры


  expect(wrapper.text()).toContain('Иванов')
  expect(wrapper.text()).toContain('Сидоров')
 
});

Это рабочая схема, но что если нам нужно протестировать поведение, когда ответ от сервера пришел с ошибкой? Допустим, для  500-ой ошибки у нас появляется тост с надписью «Сервер временно недоступен, попробуйте позже».

Тут нам как раз поможет MSW.


import { server } from '@/mocks/server';
import { http, HttpResponse } from 'msw';
import { USER_FAKE_RESPONSE } from '...fixtures'
import * as MESSAGE_MODULE from "utils"
import { GET_USERS } from '@/api/constants/APIEndPoints.js';

let wrapper

const createComponent = (params {}) => {
  wrapper = shallowMount(OurGetUsersComponent, {
    props: {
      ...params.props,
    },
    global: {
      renderStubDefaultSlot: true,
      stubs: {
        ...params.stubs,
      },
    },
  });
};

test('Обработка получения пользователей при нажатии на кнопку найти', async () => {
const spyGetUsers =  vi.spyOn(USER_API, 'getUsersRequest') // имплементация уже есть в msw и тут её дублировать не нужно

  createComponent ()
  // лучше искать так же как и пользователь  
  const buttonNode = wrapper.findAll('.button').filter(item=>item.text()=="Найти")[0] 
  await buttonNode.trigger('click');

  await flushPromises();
  expect(spyGetUsers).toHaveBeenCalled(); // Возможно этот шаг избыточен, так как пользователю важен результат

  expect(wrapper.text()).toContain(USER_FAKE_RESPONSE.items[0].lastName)
  expect(wrapper.text()).toContain(USER_FAKE_RESPONSE.items[1].lastName)
 
});

test('Обработка ошибок от сервера при получении пользователей', async ()=>{
spyMessage = vi.spyOn(MESSAGE_MODULE , 'showErrorMessage')

  server.use(
    http.get('*' + GET_USERS, () => {
      return new HttpResponse(null, { status: 500 });
    }),
  );
  createComponent ()
  const buttonNode = wrapper.findAll('.button').filter(item=>item.text()=="Найти")[0] 
  await buttonNode.trigger('click');

expect(spyMessage ).toHaveBeenCalledWith({message: 'Сервер временно не доступен, попробуйте позже' });

})

Таким образом можно сделать ваши unit-тесты чуть честнее, а возможности команды — чуть шире.

Как обычно тут мог быть мой телеграмм канал, но его нет.

Комментарии (2)


  1. matim_ioioi
    10.09.2025 01:51

    Спасибо за статью! Но так и не понял, в чём плюс использования msw в тестах :)

    В Вашем примере тест зависит от написанного хендлера в msw, о котором из теста явно не известно вообще ничего

    Да и к тому же, зачем поднимать «сервер» для тестов

    Моё мнение: каждый тест должен быть самодостаточным. Это значит, что в каждом тесте должен быть мок запроса (конкретно реализация мока зависит от того, что вы используете, будь то fetch, axios или какой-нибудь gql-клиент). Ну или в каждой группе (дескрайбе), если тест-кейсов несколько на один и тот же набор данных

    В общем так: есть фикстуры для тестов (наборы различных данных или функции для генерации нужных наборов данных); есть тест, в котором используются эти фикстуры в имплементации мока запроса; если практикуете параллельное программирование, то можно использовать ранее подготовленные для тестов фикстуры уже в хендлерах msw (ну или наоборот, если вы сначала реализуете компонент с моками на msw, а потом уже пишете тесты)

    А вот сам по себе msw хорош, особенно для тех, кто практикует tdd и контрактное программирование (tdd тут не потому, что в тестах надо использовать msw, а потому, что на этапе подготовки тестов уже будут подготовлены фикстуры, которые можно использовать в хендлерах msw)


    1. Amfeon Автор
      10.09.2025 01:51

      Благодарю). Вы правильно заметили про контрактное программирование. Изначально, все было как вы описали в каждом тесте - мок запросов. Но потом понадобилось разрабатывать независимо от бекенда и тут подвернулся msw.
      Как мне показалось писать мок в msw, а потом еще и в тесте - расточительно и было принято решение все мокирование вынести в отдельный модуль, который построен над msw, бонусом получил удобную возможность работать с заголовками, в частности внутри теста, что сохраняет понятность и то это не всегда нужно.

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

      Пока такая схема работает, продолжаю вести наблюдение =)