Как я начал писать unit-тесты для Vue. Part deux: год спустя…

от автора

Итак, прошел год с предыдущей серии, многое поменялось, из каждого утюга сообщают, что вот-вот нейронки заменят всех и вся, а я всё также тружусь во 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-тесты чуть честнее, а возможности команды — чуть шире.

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


ссылка на оригинал статьи https://habr.com/ru/articles/945168/