Тестирование во Vue: виды тестов и как писать unit-тесты
Тесты — это код, который проверяет ваш код. Вы один раз описываете, как функция или компонент должны себя вести, и дальше программа сама проверяет это при каждом запуске. Сломали что-то рефакторингом — тест сразу краснеет, и вы узнаёте о баге до того, как его увидит пользователь.
В этой статье разберём тестирование фронтенда на примере Vue: что такое unit-тесты, какие ещё бывают виды тестов, и как писать их правильно. Все примеры — на Vitest и Vue Test Utils: это стандартный современный стек для Vue 3.
Зачем вообще писать тесты
Тесты решают несколько практических задач:
- Защита от регрессий. Меняете одно — не ломаете другое: тесты ловят случайные поломки.
- Уверенность при рефакторинге. Можно смело переписывать внутренности, пока тесты зелёные.
- Живая документация. Хороший тест читается как описание поведения: «при клике счётчик увеличивается».
- Меньше ручной проверки. Не нужно каждый раз вручную кликать по интерфейсу, чтобы убедиться, что всё работает.
Главное правило: тестируем поведение, а не реализацию. Тест должен проверять «что видит и делает пользователь», а не то, какие внутренние переменные вы назвали. Тогда тесты не ломаются от любого рефакторинга и приносят пользу, а не боль.
Пирамида тестирования: какие бывают тесты
Тесты во фронтенде принято делить на уровни. Их часто рисуют в виде «пирамиды»: чем ниже уровень, тем тестов больше, тем они быстрее и дешевле.
- Unit-тесты (модульные) — проверяют маленькую изолированную часть: одну функцию, один composable, один компонент. Их много, они быстрые.
- Интеграционные тесты — проверяют, как несколько частей работают вместе: компонент + хранилище (Pinia), форма + валидация.
- E2E-тесты (end-to-end) — проверяют приложение целиком в настоящем браузере, как живой пользователь: «открыл страницу → заполнил форму → нажал отправить → увидел результат». Их мало, они медленные, но дают максимум уверенности.
Отдельно во Vue выделяют компонентные тесты — это что-то между unit и e2e: рендерим один компонент, но в настоящем браузере (например, через Cypress или Playwright Component Testing). Для большинства задач хватает unit-тестов на Vitest — с них и стоит начинать.
Что такое unit-тест на практике
Unit-тест — это функция, которая запускает кусочек вашего кода и проверяет результат. Структура почти любого теста описывается схемой AAA: Arrange (подготовь данные), Act (выполни действие), Assert (проверь результат).
Начнём с самого простого — с чистой функции, без всякого Vue:
// sum.js
export function sum(a, b) {
return a + b
}
// sum.test.js
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum', () => {
it('складывает два числа', () => {
// Arrange + Act
const result = sum(2, 3)
// Assert
expect(result).toBe(5)
})
it('работает с отрицательными числами', () => {
expect(sum(-1, -4)).toBe(-5)
})
})
Здесь видны главные «кирпичики», которые есть в любом тест-фреймворке:
describe(...)— группа связанных тестов (по функции или компоненту);it(...)(илиtest(...)) — один тест-кейс. Текст внутри — человекочитаемое описание ожидаемого поведения;expect(...)— то, что мы проверяем;.toBe(...),.toEqual(...)и т.п. — «матчеры», описывающие ожидание.
Установка и запуск
Если проект собран на Vite (а большинство свежих Vue-проектов — да), Vitest подключается в пару команд:
npm install -D vitest @vue/test-utils jsdom
# package.json → "scripts"
# "test": "vitest"
npm test # запустить в watch-режиме
npx vitest run # один прогон (для CI)
Чтобы тесты могли «рендерить» компоненты без настоящего браузера, нужна
среда jsdom — это эмуляция DOM в Node.js. Указываем её в конфиге:
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true, // describe/it/expect без импорта
},
})
Тестируем Vue-компонент
Для компонентов используют библиотеку Vue Test Utils. Её
основная функция — mount(): она «монтирует» компонент в
виртуальный DOM и возвращает обёртку (wrapper), через которую
можно искать элементы, кликать и читать текст.
Возьмём простой компонент-счётчик:
<!-- Counter.vue -->
<template>
<div>
<p>Значение: {{ count }}</p>
<button @click="increment">Увеличить</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
А вот тест к нему:
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('показывает начальное значение 0', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Значение: 0')
})
it('увеличивает счётчик по клику', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Значение: 1')
})
})
Обратите внимание на два важных момента:
-
awaitпередtrigger('click')— Vue обновляет DOM асинхронно. Безawaitвы проверите состояние ещё до перерисовки и получите старое значение. -
Мы проверяем видимый текст, а не внутреннюю переменную
count. Если завтра переименуетеcountвvalue, тест останется зелёным — потому что для пользователя ничего не изменилось.
Props, события и поиск элементов
Компоненты редко живут в вакууме: им передают входные данные (props) и они отдают наружу события (emit). Тестируется и то, и другое.
<!-- Greeting.vue -->
<template>
<button data-test="hello" @click="$emit('greet', name)">
Привет, {{ name }}!
</button>
</template>
<script setup>
defineProps({ name: String })
defineEmits(['greet'])
</script>
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Greeting from './Greeting.vue'
describe('Greeting', () => {
it('выводит имя из props', () => {
const wrapper = mount(Greeting, {
props: { name: 'Павел' },
})
expect(wrapper.text()).toContain('Привет, Павел!')
})
it('эмитит событие greet с именем при клике', async () => {
const wrapper = mount(Greeting, {
props: { name: 'Павел' },
})
await wrapper.get('[data-test="hello"]').trigger('click')
// проверяем, что событие вылетело и с каким payload
expect(wrapper.emitted('greet')).toBeTruthy()
expect(wrapper.emitted('greet')[0]).toEqual(['Павел'])
})
})
Здесь же — важная практика: для поиска элементов в тестах удобно вешать
атрибут data-test="...". Он не зависит от вёрстки и стилей:
можно сколько угодно менять классы и теги, тест по data-test не
сломается. Искать элементы по CSS-классам в тестах — плохая идея.
Моки: как изолировать тестируемый код
«Unit» значит «изолированный». Если компонент ходит в сеть или зависит от таймера, в тесте мы это подменяем заглушкой — моком. Тест не должен реально стучаться на сервер: это медленно и непредсказуемо.
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserCard from './UserCard.vue'
// подменяем модуль с запросами к API
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'Анна' }),
}))
describe('UserCard', () => {
it('показывает имя пользователя после загрузки', async () => {
const wrapper = mount(UserCard)
// ждём, пока завершатся промисы (запрос «вернёт» мок)
await flushPromises()
expect(wrapper.text()).toContain('Анна')
})
})
vi.fn() — это функция-шпион: её можно настроить на нужный
результат и потом проверить, вызывали ли её и с какими аргументами.
flushPromises() ждёт завершения всех «висящих» промисов, чтобы
DOM успел обновиться после асинхронной загрузки.
Лучшие практики написания unit-тестов
- Тестируйте поведение, а не реализацию. Проверяйте, что видит и делает пользователь (текст, клики, события), а не имена внутренних переменных и приватных методов.
-
Один тест — одна мысль. Каждый
it()должен проверять что-то одно. Если в названии теста есть «и» — возможно, это два теста. -
Понятные названия. Описание в
it(...)читается как фраза: «увеличивает счётчик по клику», а не «test 1». - Структура AAA. Подготовка → действие → проверка. Так тест легко читать даже спустя полгода.
-
Тесты независимы. Порядок запуска не должен влиять на
результат. Общее состояние сбрасывайте в
beforeEach. - Не мокайте лишнего. Подменяйте только внешние границы (сеть, время, сторонние сервисы). Если замокать слишком много — тест будет проверять заглушки, а не ваш код.
- Тесты должны быть быстрыми и стабильными. «Мигающий» тест, который то падает, то проходит, хуже отсутствия теста — ему перестают верить.
- Гонитесь не за процентом покрытия, а за смыслом. 100% покрытия не гарантируют отсутствие багов. Покрывайте важную логику и граничные случаи, а не геттеры ради цифры.
Что писать в тесте в первую очередь
Если не знаете, с чего начать, проверяйте эти вещи:
- компонент рендерится с разными props (включая пустые и граничные значения);
- пользовательские действия (клик, ввод в поле) меняют интерфейс как ожидается;
- наружу вылетают нужные события с правильными данными;
- условный рендеринг: показывается ли блок, когда он должен, и скрыт ли, когда не должен;
- обработка ошибок и состояния загрузки (спиннер, сообщение об ошибке).
Итог
Тестирование во фронтенде делится на уровни: много быстрых unit-тестов, меньше интеграционных и совсем немного медленных e2e. Для Vue базовый и самый окупаемый уровень — это unit-тесты на Vitest и Vue Test Utils.
Начните с малого: подключите Vitest, напишите тест для одной чистой функции, потом для простого компонента — проверьте рендер, клик и событие. Держите в голове одно правило — тестируйте поведение, а не реализацию — и ваши тесты будут помогать, а не мешать. Лучший способ разобраться — открыть свой проект и покрыть тестами хотя бы один компонент уже сегодня.