Vue
Тестирование: Vitest и Playwright
Форма оформления заказа работала полгода, пока рефакторинг компонента кнопки не перестал эмитить событие submit. На проде это всплыло жалобой: пользователи нажимают Оплатить, ничего не происходит. Юнит-тест на тридцать строк, который монтирует кнопку, кликает и проверяет emitted, поймал бы регресс за миллисекунды до деплоя. А end-to-end тест на Playwright прошёл бы весь путь от корзины до подтверждения в настоящем браузере. Этот урок про две границы тестирования: Vitest с Vue Test Utils для компонентов и композаблов, и Playwright для сквозных сценариев.
- Vitest: дефолтный тест-раннер для проектов на Vite, использует тот же конвейер трансформации
- Vue Test Utils: mount и shallowMount монтируют компонент в тесте и дают доступ к его выводу
- findComponent и emitted: проверка взаимодействия с дочерними компонентами и испускаемых событий
- Тестирование композаблов: чистая логика из useX проверяется без рендеринга компонента
- Playwright: end-to-end тесты прогоняют реальный сценарий в Chromium, Firefox и WebKit
Предварительные знания
- Понимание компонентов SFC, их пропсов и событий
- Базовое знание композаблов как функций с реактивным состоянием
- Идея разницы между юнит-тестом и сквозным тестом
Vitest и монтирование компонента
Vitest это тест-раннер, построенный на том же конвейере, что и Vite. Он переиспользует резолвинг модулей, трансформацию SFC и алиасы из конфига приложения, поэтому тесты видят код ровно так же, как видит его сборка. API совместим с Jest (describe, it, expect), но запуск быстрее за счёт esbuild и горячего перезапуска только затронутых тестов.
Чтобы протестировать компонент, его монтируют через Vue Test Utils. Функция mount создаёт полноценный экземпляр и рендерит всё дерево, включая дочерние компоненты. Возвращается wrapper: обёртка с методами для поиска элементов, имитации событий и проверки вывода. Альтернатива это shallowMount, который заглушает дочерние компоненты стабами и рендерит только тестируемый, изолируя его от детей.
| Функция | Что делает | Когда выбирать |
|---|---|---|
| mount | Рендерит компонент со всеми детьми | Проверка интеграции с дочерними компонентами |
| shallowMount | Заглушает детей стабами | Изоляция логики только тестируемого компонента |
Имитация событий и обновление DOM в Vue асинхронны: после trigger или изменения состояния DOM обновляется на следующем тике. Поэтому методы вроде trigger возвращают промис, и перед проверкой результата его ожидают через await. Без await тест проверит DOM до обновления и упадёт без видимой причины.
Чем mount отличается от shallowMount в Vue Test Utils?
findComponent и проверка emitted
Контракт компонента это его пропсы на входе и события на выходе. Чтобы проверить выход, Vue Test Utils записывает все испускаемые события в wrapper.emitted(). Метод возвращает объект, где ключ это имя события, а значение это массив вызовов, каждый со своими аргументами. Так тест убеждается, что компонент эмитит нужное событие с правильной нагрузкой именно тогда, когда должен.
Когда нужно добраться до дочернего компонента (проверить переданный ему проп или испускаемое им событие), используется findComponent. В отличие от find, который ищет по CSS-селектору, findComponent находит экземпляр компонента по его определению. Это даёт типизированный доступ к props и emitted конкретного ребёнка.
emitted без аргументов возвращает все события компонента, а с именем только указанное. Возвращаемое значение может быть undefined, если событие ни разу не испускалось, поэтому к результату обращаются через optional chaining, а не предполагают, что массив точно есть.
Тест должен убедиться, что компонент эмитит событие submit с введённым значением. Какой инструмент Vue Test Utils это проверяет?
Тестирование композаблов и Playwright
Композабл это обычная функция, которая возвращает реактивное состояние и методы. Если он не зависит от жизненного цикла компонента, его тестируют простым вызовом: дёргают функцию, меняют состояние, проверяют результат. Никакого монтирования не нужно, потому что вся логика живёт в самой функции, а не в шаблоне.
Если композабл использует хуки жизненного цикла (onMounted, onUnmounted) или provide и inject, прямой вызов не сработает, ведь эти API требуют активного экземпляра компонента. Тогда композабл оборачивают в тестовый компонент и монтируют его через Vue Test Utils, проверяя поведение уже внутри контекста.
Юнит-тесты проверяют отдельные единицы в изоляции. Но они не гарантируют, что приложение целиком работает: роутинг, реальные запросы, рендеринг в настоящем браузере. Это задача end-to-end тестов на Playwright. Он запускает приложение в реальном Chromium, Firefox или WebKit, кликает как пользователь, ждёт навигации и проверяет видимое на странице. Playwright медленнее юнит-тестов, поэтому им покрывают критические сценарии целиком, а не каждую кнопку.
| Уровень | Инструмент | Что проверяет |
|---|---|---|
| Юнит | Vitest, прямой вызов | Логику композабла или функции |
| Компонент | Vitest, Vue Test Utils | Рендер, пропсы, события компонента |
| Сквозной | Playwright | Полный сценарий в реальном браузере |
Соблазн покрыть всё сквозными тестами заканчивается медленным и хрупким набором: каждый прогон гоняет браузер, а любая мелкая правка верстки ломает селекторы. Базу покрытия дают быстрые юнит и компонентные тесты, а Playwright оставляют для немногих ключевых пользовательских путей.
Композабл useCounter возвращает count, increment и reset и не использует хуки жизненного цикла. Как его тестировать?
Связь с другими темами
Урок опирается на компоненты и типизацию и связан со сборкой:
- Компоненты SFC — Тестируется именно компонент: его рендер, пропсы и события
- TypeScript в Vue — Типизированный контракт пропсов и эмитов задаёт, что именно проверять в тесте
- Сборка: Vite 8 — Vitest переиспользует трансформацию Vite, поэтому конфиг общий с приложением
Итог
- Vitest это тест-раннер на конвейере Vite: тот же резолвинг и трансформация, что у приложения, плюс быстрый watch-режим
- Vue Test Utils монтирует компонент в тесте: mount рендерит и дочерние компоненты, shallowMount заглушает их стабами
- wrapper.find и wrapper.findComponent находят DOM-элемент или дочерний компонент, а trigger имитирует событие
- wrapper.emitted возвращает зафиксированные испускаемые события с их аргументами, что проверяет контракт компонента
- Композабл это обычная функция с реактивным состоянием, поэтому он тестируется вызовом напрямую без монтирования компонента
- Playwright прогоняет сквозной сценарий в реальном браузере, проверяя поведение всего приложения, а не отдельной единицы
Связанные уроки
- vue-40-typescript-vue — Типизированный контракт пропсов и эмитов делает тесты точными и проверяемыми
- vue-43-build-tooling — Vitest использует тот же конвейер трансформации Vite, что и сборка приложения