Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Тестирование Vue компонентов
vue_testing

Тестирование Vue компонентов

Vitest, Vue Test Utils: mount/shallowMount, моки props и emits, тестирование composables и асинхронных операций

Тестирование Vue: Vitest + Vue Test Utils

Тестирование фронтенда — это не про стопроцентное покрытие кода. Это про уверенность: вы можете рефакторить, добавлять фичи и быть уверены, что ничего не сломалось. Хорошие тесты описывают поведение компонента с точки зрения пользователя, а не детали реализации.

#Что и зачем тестировать

Три уровня тестирования фронтенда:

Unit-тесты — изолированное тестирование composables, утилитарных функций, хранилищ Pinia. Быстрые (миллисекунды), много таких тестов. Проверяют логику без UI.

Component-тесты — монтирование компонента в виртуальный DOM, проверка рендеринга, взаимодействий, событий. Медленнее unit-тестов, но проверяют интеграцию логики с шаблоном.

E2E-тесты — запуск реального браузера, тестирование полных сценариев. Самые медленные и хрупкие, но проверяют всё вместе включая бэкенд.

Правило пирамиды: много unit, меньше component, совсем мало e2e.

#Установка и настройка Vitest

Vitest — это тест-раннер от создателей Vite. Он использует ту же конфигурацию Vite, понимает Vue SFC, поддерживает TypeScript без дополнительной настройки:

npm install -D vitest @vue/test-utils jsdom @vitejs/plugin-vue

Конфигурация vitest.config.ts:

import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', // Имитация браузерного DOM globals: true, // describe, it, expect без импорта setupFiles: ['./src/test/setup.ts'], }, })

Файл src/test/setup.ts для глобальных настроек:

import { config } from '@vue/test-utils' // Глобальные стабы для компонентов, которые не нужно тестировать config.global.stubs = { RouterLink: true, RouterView: true, }

Добавьте скрипты в package.json:

{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage" } }

#Первый unit-тест: тестирование composable

Composables тестировать проще всего — это обычные функции. Вам нужно только правильно инициализировать контекст Vue:

// composables/useCounter.ts import { ref } from 'vue' export function useCounter(initial: number = 0) { const count = ref(initial) const increment = () => count.value++ const decrement = () => count.value-- const reset = () => { count.value = initial } return { count, increment, decrement, reset } }
// composables/useCounter.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { useCounter } from './useCounter' describe('useCounter', () => { it('инициализируется с начальным значением', () => { const { count } = useCounter(5) expect(count.value).toBe(5) }) it('инициализируется нулём по умолчанию', () => { const { count } = useCounter() expect(count.value).toBe(0) }) it('увеличивает счётчик', () => { const { count, increment } = useCounter() increment() expect(count.value).toBe(1) }) it('уменьшает счётчик', () => { const { count, decrement } = useCounter(3) decrement() expect(count.value).toBe(2) }) it('сбрасывает к начальному значению', () => { const { count, increment, reset } = useCounter(10) increment() increment() reset() expect(count.value).toBe(10) }) })

#mount vs shallowMount

mount монтирует компонент со всеми дочерними компонентами. shallowMount заменяет дочерние компоненты заглушками.

Когда использовать mount:

  • Тестируете взаимодействие компонента с дочерними
  • Интеграционные тесты
  • Дочерние компоненты простые (кнопки, inputs)

Когда использовать shallowMount:

  • Хотите изолировать тестируемый компонент
  • Дочерние компоненты сложные или делают HTTP-запросы
  • Тестируете только логику родительского компонента
<!-- Button.vue --> <script setup lang="ts"> defineProps<{ label: string; disabled?: boolean }>() const emit = defineEmits<{ click: [] }>() </script> <template> <button :disabled="disabled" class="btn" @click="emit('click')" > {{ label }} </button> </template>
// Button.test.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import Button from './Button.vue' describe('Button', () => { it('отображает label', () => { const wrapper = mount(Button, { props: { label: 'Нажми меня' } }) expect(wrapper.text()).toContain('Нажми меня') }) it('заблокирован когда disabled=true', () => { const wrapper = mount(Button, { props: { label: 'OK', disabled: true } }) expect(wrapper.find('button').attributes('disabled')).toBeDefined() }) it('не заблокирован по умолчанию', () => { const wrapper = mount(Button, { props: { label: 'OK' } }) expect(wrapper.find('button').attributes('disabled')).toBeUndefined() }) })

#Доступ к DOM: find, text, html

Vue Test Utils предоставляет API для взаимодействия с виртуальным DOM:

const wrapper = mount(MyComponent) // Поиск элементов wrapper.find('.submit-btn') // первый элемент (возвращает DOMWrapper) wrapper.findAll('li') // все совпадения wrapper.findComponent(ChildComponent) // поиск Vue компонента // Проверки состояния wrapper.text() // весь текстовый контент wrapper.html() // HTML строка wrapper.exists() // true/false существует ли элемент wrapper.isVisible() // учитывает display:none и visibility // Атрибуты и классы wrapper.find('input').attributes('placeholder') wrapper.find('.card').classes() // массив классов wrapper.find('.card').classes('active') // true/false наличие класса

#Тестирование событий

Тестирование кликов, ввода и пользовательских событий — ключевая часть component-тестов:

import { mount, flushPromises } from '@vue/test-utils' import { describe, it, expect, vi } from 'vitest' import SearchInput from './SearchInput.vue' describe('SearchInput', () => { it('эмитит событие search при нажатии кнопки', async () => { const wrapper = mount(SearchInput) // Симуляция ввода await wrapper.find('input').setValue('vue router') // Клик по кнопке await wrapper.find('[data-test="search-btn"]').trigger('click') // Проверка эмитированных событий expect(wrapper.emitted('search')).toBeTruthy() expect(wrapper.emitted('search')?.[0]).toEqual(['vue router']) }) it('не эмитит при пустом поле', async () => { const wrapper = mount(SearchInput) await wrapper.find('[data-test="search-btn"]').trigger('click') expect(wrapper.emitted('search')).toBeFalsy() }) })

Важно: все действия с DOM (trigger, setValue) возвращают Promise — всегда используйте await. Иначе проверки выполнятся до того как Vue обновит DOM.

#Тестирование slots

Слоты позволяют передать контент в компонент. Тестирование слотов в Vue Test Utils:

import { mount } from '@vue/test-utils' import Card from './Card.vue' it('рендерит default slot', () => { const wrapper = mount(Card, { slots: { default: '<p class="slot-content">Контент карточки</p>' } }) expect(wrapper.find('.slot-content').exists()).toBe(true) expect(wrapper.find('.slot-content').text()).toBe('Контент карточки') }) it('рендерит named slots', () => { const wrapper = mount(Card, { slots: { header: '<h2>Заголовок</h2>', default: 'Основной текст', footer: '<button>Действие</button>' } }) expect(wrapper.find('h2').text()).toBe('Заголовок') expect(wrapper.text()).toContain('Основной текст') })

#Мокирование зависимостей

Иногда компонент использует fetch, localStorage или внешние модули. vi.mock() позволяет заменить их на контролируемые заглушки:

import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import UserProfile from './UserProfile.vue' // Мокируем весь модуль vi.mock('@/api/users', () => ({ fetchUser: vi.fn() })) import { fetchUser } from '@/api/users' describe('UserProfile', () => { beforeEach(() => { // Сбрасываем состояние мока перед каждым тестом vi.clearAllMocks() }) it('показывает данные пользователя после загрузки', async () => { // Настраиваем что вернёт мок vi.mocked(fetchUser).mockResolvedValueOnce({ id: 1, name: 'Иван Петров', email: 'ivan@example.com' }) const wrapper = mount(UserProfile, { props: { userId: 1 } }) // Ждём завершения всех промисов await flushPromises() expect(wrapper.find('[data-test="user-name"]').text()).toBe('Иван Петров') }) it('показывает ошибку при неудачной загрузке', async () => { vi.mocked(fetchUser).mockRejectedValueOnce(new Error('Сервер недоступен')) const wrapper = mount(UserProfile, { props: { userId: 1 } }) await flushPromises() expect(wrapper.find('[data-test="error-message"]').exists()).toBe(true) }) })

#Тестирование Pinia

Pinia требует установки перед использованием в тестах. setActivePinia создаёт изолированный экземпляр для каждого теста:

import { describe, it, expect, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useCartStore } from './cart' describe('useCartStore', () => { beforeEach(() => { // Создаём свежую Pinia для каждого теста setActivePinia(createPinia()) }) it('добавляет товар в корзину', () => { const cart = useCartStore() cart.addItem({ id: 1, name: 'Книга', price: 500 }) expect(cart.items).toHaveLength(1) expect(cart.totalPrice).toBe(500) }) it('считает сумму нескольких товаров', () => { const cart = useCartStore() cart.addItem({ id: 1, name: 'Книга', price: 500 }) cart.addItem({ id: 2, name: 'Курс', price: 1500 }) expect(cart.totalPrice).toBe(2000) }) })

При тестировании компонентов, которые используют Pinia, передайте плагин через global.plugins:

const wrapper = mount(CartSummary, { global: { plugins: [createPinia()] } })

#flushPromises и async тесты

flushPromises из @vue/test-utils ожидает завершения всех очередей промисов. Это критично для тестирования компонентов с async/await и onMounted:

import { flushPromises } from '@vue/test-utils' it('загружает список после монтирования', async () => { const wrapper = mount(PostList) // Компонент только что смонтирован, onMounted ещё выполняется expect(wrapper.find('[data-test="loading"]').exists()).toBe(true) // Ждём все промисы await flushPromises() // Теперь данные загружены expect(wrapper.find('[data-test="loading"]').exists()).toBe(false) expect(wrapper.findAll('[data-test="post-item"]')).toHaveLength(3) })

#Coverage: анализ покрытия

Покрытие показывает какие строки кода выполнялись в тестах. Установка @vitest/coverage-v8:

npm install -D @vitest/coverage-v8

Конфигурация в vitest.config.ts:

export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'html', 'json'], thresholds: { lines: 80, functions: 80, branches: 70, }, exclude: ['src/test/**', '**/*.test.ts', 'src/main.ts'] } } })

Запуск: npm run test:coverage. Отчёт в coverage/index.html показывает непокрытые строки. Но помните: 100% покрытие — не цель. Тест который просто "проходит через" код без проверок создаёт ложное ощущение безопасности.

#Playwright для e2e тестирования

Playwright — современный инструмент для e2e тестов. Поддерживает Chromium, Firefox, WebKit:

npm init playwright@latest

Первый e2e тест:

// tests/auth.spec.ts import { test, expect } from '@playwright/test' test('пользователь может войти в систему', async ({ page }) => { await page.goto('/login') // Заполняем форму await page.fill('[data-test="email-input"]', 'user@example.com') await page.fill('[data-test="password-input"]', 'password123') // Нажимаем кнопку await page.click('[data-test="submit-btn"]') // Проверяем результат await expect(page).toHaveURL('/dashboard') await expect(page.locator('[data-test="user-menu"]')).toBeVisible() }) test('показывает ошибку при неверном пароле', async ({ page }) => { await page.goto('/login') await page.fill('[data-test="email-input"]', 'user@example.com') await page.fill('[data-test="password-input"]', 'wrongpass') await page.click('[data-test="submit-btn"]') await expect(page.locator('.error-message')).toContainText('Неверный пароль') })

Запуск: npx playwright test. Интерактивный режим: npx playwright test --ui.

Важный паттерн: используйте атрибуты data-test="..." для тестовых селекторов. Не привязывайтесь к CSS-классам — они меняются при рефакторинге стилей.

Далее: Pinia: управление состоянием