Тестування Vue 3 у 2026 році: Vitest, Vue Test Utils та питання співбесід
Практичний посібник із тестування Vue у 2026 році: налаштування Vitest, монтування компонентів через Vue Test Utils, перевірка композаблів і сховищ Pinia, мокування API, вимірювання покриття та питання, які ставлять команди на співбесідах.

Тестування Vue відділяє впевнений рефакторинг від крихких здогадок, і у 2026 році інструментарій сконцентрувався навколо двох бібліотек: Vitest як тестового раннера та Vue Test Utils для монтування компонентів. Цей посібник послідовно розглядає налаштування стека, тестування компонентів і композаблів, мокування залежностей, а також відповіді на ті питання про тестування Vue, які команди справді ставлять на співбесідах.
Стандартна рекомендація з документації Vue — це Vitest для юніт- та компонентних тестів, Vue Test Utils як низькорівневий API монтування, а також Playwright або Cypress для наскрізних (end-to-end) сценаріїв. Vitest перевикористовує ту саму конфігурацію Vite, що й застосунок, тож тести проходять через ідентичний конвеєр трансформації з майже миттєвим гарячим перезавантаженням.
Налаштування Vitest для проєкту Vue 3
Vitest використовує спільний із Vite конвеєр, а отже один конфігураційний файл керує і сервером розробки, і тестовим раннером. Параметр environment має бути встановлений у jsdom або happy-dom, щоб компонентні тести мали DOM, у який можна рендерити. Прапорець globals: true робить describe, it та expect доступними без імпорту в кожному файлі.
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
export default defineConfig({
plugins: [vue()],
test: {
// jsdom provides document/window so components can mount
environment: 'jsdom',
// expose describe/it/expect globally
globals: true,
// run this before each test file (matchers, cleanup)
setupFiles: ['./tests/setup.ts'],
// only pick up *.spec.ts and *.test.ts files
include: ['**/*.{test,spec}.ts'],
},
resolve: {
alias: {
// mirror the @ alias used across the app
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})У Vitest 3 наведена конфігурація працює без додаткових зусиль після встановлення vitest, @vue/test-utils, @vitejs/plugin-vue та jsdom. Оскільки аліас дублює конфігурацію застосунку, імпорти на кшталт @/composables/useCart розв'язуються однаково і в тестах, і в продакшені.
Монтування компонентів через Vue Test Utils
Vue Test Utils надає дві точки входу: mount, що рендерить повне дерево компонента, та shallowMount, що замінює дочірні компоненти заглушками. Для більшості юніт-тестів перевагу має mount, бо він задіює реальну поведінку рендерингу. Повернутий wrapper надає допоміжні методи запитів на зразок find, get та text для перевірки відрендереного результату.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PriceTag from '@/components/PriceTag.vue'
describe('PriceTag', () => {
it('formats the amount as currency', () => {
const wrapper = mount(PriceTag, {
// props are passed through the mounting options
props: { amount: 4200, currency: 'EUR' },
})
// get() throws if the selector is missing, unlike find()
expect(wrapper.get('[data-test="price"]').text()).toBe('€42.00')
})
it('applies a discount class when on sale', () => {
const wrapper = mount(PriceTag, {
props: { amount: 4200, currency: 'EUR', onSale: true },
})
// classes() returns the array of applied CSS classes
expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale')
})
})Орієнтація на елементи через атрибути data-test, а не через CSS-класи, робить тести стійкими: зміна стилів у .price--sale не зламає селектор, це зробить лише справжня зміна поведінки. Таке розчеплення є наскрізною темою підтримуваного тестування Vue.
Тестування подій користувача та еміттованих подій
Інтерактивні компоненти потребують перевірок того, що відбувається після кліку або введення. Vue Test Utils повертає проміс із виклику trigger, і його очікування скидає чергу реактивності Vue, щоб DOM відобразив оновлення. Допоміжний метод emitted записує кожну кастомну подію, яку викликав компонент, і саме так перевіряються контракти між батьківським і дочірнім компонентами.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchBar from '@/components/SearchBar.vue'
describe('SearchBar', () => {
it('emits search with the typed query', async () => {
const wrapper = mount(SearchBar)
// setValue writes to the input and fires an input event
await wrapper.get('input').setValue('vitest')
// awaiting trigger flushes the reactivity queue before assertions
await wrapper.get('[data-test="submit"]').trigger('click')
// emitted() returns a record of every event and its payloads
const events = wrapper.emitted('search')
expect(events).toHaveLength(1)
// events[0] is the first emission, [0] its first argument
expect(events![0][0]).toBe('vitest')
})
})Vue групує реактивні оновлення асинхронно. Забутий await перед викликом trigger чи setValue — це найпоширеніша причина нестабільних тестів Vue: перевірки виконуються до того, як DOM перерендериться, тож вони читають застарілий результат. Коли зміна стану відбувається поза допоміжним методом для подій, слід імпортувати nextTick із vue і виконати await nextTick() перед перевіркою.
Тестування композаблів Vue в ізоляції
Композабли, які використовують лише API реактивності (ref, computed, watch), не потребують жодного компонента: це звичайні функції, що повертають реактивний стан, тож достатньо викликати їх напряму й перевіряти .value. Це найшвидший і найточніший спосіб покрити бізнес-логіку, і він добре поєднується з підходами з просунутих композаблів Vue 3.
import { describe, it, expect } from 'vitest'
import { useCart } from '@/composables/useCart'
describe('useCart', () => {
it('tracks total price as items change', () => {
// composables using only ref/computed run without mounting
const { items, total, addItem } = useCart()
expect(total.value).toBe(0)
addItem({ id: 1, price: 1500, qty: 2 })
// computed total recalculates synchronously on state change
expect(total.value).toBe(3000)
addItem({ id: 2, price: 500, qty: 1 })
expect(items.value).toHaveLength(2)
expect(total.value).toBe(3500)
})
})Винятком є композабли, що залежать від хуків життєвого циклу на кшталт onMounted: вони мають виконуватися всередині екземпляра компонента. Поширений обхідний прийом — крихітний допоміжний компонент, який викликає композабл у setup і надає результат назовні, після чого цей помічник монтується через Vue Test Utils.
Готовий до співбесід з Vue.js / Nuxt.js?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Мокування викликів API через vi.mock
Справжні мережеві запити роблять тести повільними й недетермінованими. Vitest замінює модулі за допомогою vi.mock і створює шпигунські функції через vi.fn. Наведений нижче патерн мокує модуль-обгортку навколо fetch, тож композабл, який тестується, отримує контрольовані дані, жодного разу не звертаючись до сервера.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useProducts } from '@/composables/useProducts'
import { getProducts } from '@/api/products'
// replace the whole module with mocked exports
vi.mock('@/api/products', () => ({
getProducts: vi.fn(),
}))
describe('useProducts', () => {
beforeEach(() => {
// reset call history between tests to avoid leakage
vi.mocked(getProducts).mockReset()
})
it('exposes fetched products and clears loading', async () => {
// define what the mocked API returns for this test
vi.mocked(getProducts).mockResolvedValue([
{ id: 1, name: 'Keyboard' },
])
const { products, loading, load } = useProducts()
await load()
expect(getProducts).toHaveBeenCalledOnce()
expect(loading.value).toBe(false)
expect(products.value[0].name).toBe('Keyboard')
})
})Мокування на межі модуля залишає внутрішню логіку композабла недоторканою, водночас даючи кожному тесту повний контроль над відповіддю API, включно з гілками помилок. Виклик mockRejectedValue натомість дозволяє окремому тесту перевірити, що збій встановлює стан помилки і зупиняє індикатор завантаження.
Тестування сховищ Pinia у 2026 році
Сховища тримають стан, спільний для кількох компонентів, і офіційний пакет @pinia/testing робить їх зручними для тестування. createTestingPinia за замовчуванням замінює кожну дію заглушкою, тож компонентний тест може перевірити, що дію було відправлено, не запускаючи її побічних ефектів. Посібник із тестування Pinia рекомендує саме такий підхід для тестів рівня компонента.
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CheckoutButton from '@/components/CheckoutButton.vue'
import { useCartStore } from '@/stores/cart'
describe('CheckoutButton', () => {
it('dispatches checkout on click', async () => {
const wrapper = mount(CheckoutButton, {
global: {
plugins: [
// vi.fn spies on actions instead of executing them
createTestingPinia({ createSpy: vi.fn }),
],
},
})
const store = useCartStore()
await wrapper.get('[data-test="checkout"]').trigger('click')
// the action is stubbed, so only the dispatch is asserted
expect(store.checkout).toHaveBeenCalledOnce()
})
})Для тестування самої логіки сховища, а не інтеграції з компонентом, слід передати stubActions: false, щоб дії виконувалися нормально, а потім перевіряти отриманий стан. Такий поділ на компонентні тести із заглушеними діями та тести сховища зі справжніми діями тримає кожен набір тестів сфокусованим на одній відповідальності.
Запуск тестів у режимі спостереження та вимірювання покриття
Vitest постачається з режимом спостереження, який перезапускає лише ті тести, на які впливає збережений файл, і це скорочує цикл зворотного зв'язку під час розробки до частки секунди. Звітування про покриття вбудоване через провайдер @vitest/coverage-v8 і чисто інтегрується в конвеєр безперервної інтеграції, де провалений тест або регресія покриття можуть заблокувати злиття.
export default defineConfig({
test: {
coverage: {
// v8 is the fastest provider and needs no instrumentation step
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
// fail CI when a threshold drops below the target
thresholds: {
statements: 80,
branches: 75,
functions: 80,
},
// exclude generated and config files from the report
exclude: ['**/*.config.ts', '**/types/**'],
},
},
})Запуск самого лише vitest стартує режим спостереження локально, тоді як vitest run --coverage виконує єдиний прохід, придатний для CI. Встановлення явних порогів перетворює покриття з показника заради марнославства на примусову межу якості: пул-реквест, що опускає покриття гілок нижче 75 відсотків, провалюється автоматично, а це підштовхує учасників тестувати ті шляхи, які вони додають, а не лише щасливий сценарій.
Поширені питання співбесід про тестування Vue та відповіді
Питання про тестування виринають на більшості співбесід для сеньйор-рівня Vue, бо вони розкривають, як кандидат мислить про підтримуваність коду. Відповіді нижче охоплюють концепції, що трапляються найчастіше, а повний набір відпрацьовується в модулі співбесід із тестування Vue.
Коли слід використовувати shallowMount замість mount? Варто звернутися до shallowMount, коли компонент рендерить важкі або вже протестовані дочірні елементи, а тест цікавить лише власна логіка батьківського компонента. Заміна дочірніх елементів заглушками ізолює юніт і пришвидшує рендеринг ціною того, що реальна інтеграція між компонентами не перевіряється.
Як обробляються асинхронні оновлення у Vue Test Utils? Vue застосовує реактивні зміни на наступному тіку. Допоміжні методи для подій, такі як trigger і setValue, повертають проміси, що вирішуються після оновлення DOM, тож їхнього очікування достатньо. Для стану, зміненого поза цими методами, await nextTick() примушує чергу скинутися перед виконанням перевірок.
Яка різниця між find і get? find повертає порожній wrapper, коли жоден елемент не збігається, і ніколи не кидає виняток, що підходить для перевірок на кшталт expect(wrapper.find('.error').exists()).toBe(false). get негайно кидає виняток на відсутньому елементі, що робить його кращим вибором, коли елемент має існувати, а його відсутність є справжнім збоєм.
Чому варто надавати перевагу атрибутам data-test, а не селекторам за класами? Імена класів змінюються разом зі стилями та структурою, тож тести, що звертаються до них, ламаються на косметичних правках. Виділений гачок data-test явно виражає намір тестування і переживає рефакторинги, зменшуючи кількість хибних спрацювань.
Як тестується композабл, що покладається на onMounted? Хуки життєвого циклу спрацьовують лише всередині екземпляра компонента, тож композабл не можна викликати напряму. Стандартна техніка — одноразовий допоміжний компонент, який викликає композабл у setup і надає його повернуте значення назовні, після чого цей помічник монтується через Vue Test Utils, а перевірки виконуються через wrapper. Це зберігає прив'язку реактивності й життєвого циклу цілою, водночас ізолюючи логіку, яка тестується. Більше концептуальних питань і завдань на кодування зібрано в посібнику ключові питання співбесід із Vue.js.
Висновок
Надійне налаштування тестування Vue у 2026 році зводиться до кількох свідомих рішень:
- Запускати Vitest з
environment: 'jsdom'і плагіном Vue, щоб тести використовували точно той самий конвеєр трансформації Vite, що й застосунок. - Надавати перевагу
mountнадshallowMount, якщо тільки дочірній компонент не є важким або вже покритим деінде. - Завжди очікувати
trigger,setValueтаnextTickперед перевіркою, щоб усунути нестабільні, залежні від часу тести. - Тестувати композабли, що використовують лише API реактивності, викликаючи їх напряму й перевіряючи
.value. - Мокувати на межі модуля через
vi.mock, щоб мережева поведінка була детермінованою, а гілки помилок піддавалися тестуванню. - Використовувати
createTestingPinia({ createSpy: vi.fn })для компонентних тестів іstubActions: false, коли задіюється логіка сховища. - Звертатися до елементів через атрибути
data-test, щоб тести ламалися лише на справжніх змінах поведінки, а не на стилях.
З опануванням цих патернів тестування стає радше інструментом проєктування, ніж рутиною, ловлячи регресії ще до того, як вони дістануться продакшену. Досліджуйте повний навчальний шлях Vue та Nuxt, щоб і далі розвивати навички, готові до співбесід.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Просунуті Vue 3 Composables: патерни повторного використання та питання для співбесіди 2026
Вичерпний посібник з просунутих Vue 3 Composables: патерни повторного використання, асинхронна обробка помилок, Dependency Injection, валідація форм та актуальні питання для технічних співбесід у 2026 році.

Nuxt 4 у 2026 році: Нова структура каталогів та міграція з Nuxt 3
Повний посібник з міграції на Nuxt 4: нова структура app/, singleton-шар отримання даних, поверхнева реактивність за замовчуванням, розділення TypeScript-контексту, нормалізовані імена компонентів, Vue Router v5, зміни в управлінні head та контрольний список міграції.

Vue 3 Pinia vs Vuex: Сучасне управління станом та питання для співбесід 2026
Порівняння Pinia та Vuex: архітектура, TypeScript, Composition API, міграція, гідратація SSR та найпоширеніші питання зі співбесід щодо управління станом Vue у 2026 році.