Как я начал писать unit-тесты для Vue. Part deux: год спустя…
- среда, 10 сентября 2025 г. в 00:00:09
Итак, прошел год с предыдущей серии, многое поменялось, из каждого утюга сообщают, что вот-вот нейронки заменят всех и вся, а я всё также тружусь во 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, в котором генерировались скриншоты всех страниц.
Затем, накатывая очередную мажорную версию библиотеки, просто запускал сравнение текущего вида с эталонным и получал вот таких красавчиков (напомню, что тут изображено смещение элементов относительно эталонного изображения):
Правим и накатываем следующую версию. Таким образом, миграция прошла, как мне показалось, без сюрпризов. Точнее, все места были подсвечены и поправлены.
Кажется, это тот кейс, в котором нейронка сильно сократила написание однотипного кода. Да и скриншоты тут очень хорошо подошли (в продакшене из-за своей хрупкости скриншоты у меня не прижились).
В общем и целом, я сконцентрировался на юнитах (они же интеграционные в классическом понимании). Они быстрые, изолированные, простые и надежные.
Для мокирования работы с сетью подключил 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-тесты чуть честнее, а возможности команды — чуть шире.
Как обычно тут мог быть мой телеграмм канал, но его нет.