Тестування Vue 3 у 2026 році: Vitest, Vue Test Utils та питання співбесід

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

Робочий процес тестування Vue із Vitest та Vue Test Utils у 2026 році

Тестування Vue відділяє впевнений рефакторинг від крихких здогадок, і у 2026 році інструментарій сконцентрувався навколо двох бібліотек: Vitest як тестового раннера та Vue Test Utils для монтування компонентів. Цей посібник послідовно розглядає налаштування стека, тестування компонентів і композаблів, мокування залежностей, а також відповіді на ті питання про тестування Vue, які команди справді ставлять на співбесідах.

Сучасний стек тестування Vue у 2026 році

Стандартна рекомендація з документації 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 доступними без імпорту в кожному файлі.

vitest.config.tstypescript
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 для перевірки відрендереного результату.

PriceTag.spec.tstypescript
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 записує кожну кастомну подію, яку викликав компонент, і саме так перевіряються контракти між батьківським і дочірнім компонентами.

SearchBar.spec.tstypescript
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')
  })
})
Завжди очікуйте оновлення DOM

Vue групує реактивні оновлення асинхронно. Забутий await перед викликом trigger чи setValue — це найпоширеніша причина нестабільних тестів Vue: перевірки виконуються до того, як DOM перерендериться, тож вони читають застарілий результат. Коли зміна стану відбувається поза допоміжним методом для подій, слід імпортувати nextTick із vue і виконати await nextTick() перед перевіркою.

Тестування композаблів Vue в ізоляції

Композабли, які використовують лише API реактивності (ref, computed, watch), не потребують жодного компонента: це звичайні функції, що повертають реактивний стан, тож достатньо викликати їх напряму й перевіряти .value. Це найшвидший і найточніший спосіб покрити бізнес-логіку, і він добре поєднується з підходами з просунутих композаблів Vue 3.

useCart.spec.tstypescript
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, тож композабл, який тестується, отримує контрольовані дані, жодного разу не звертаючись до сервера.

useProducts.spec.tstypescript
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 рекомендує саме такий підхід для тестів рівня компонента.

CheckoutButton.spec.tstypescript
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 і чисто інтегрується в конвеєр безперервної інтеграції, де провалений тест або регресія покриття можуть заблокувати злиття.

vitest.config.ts (coverage section)typescript
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
#testing
#vitest
#vue-test-utils
#interview

Поділитися

Пов'язані статті