Testowanie Vue 3 w 2026: Vitest, Vue Test Utils i pytania rekrutacyjne

Praktyczny przewodnik po testowaniu Vue w 2026: konfiguracja Vitest, montowanie komponentów z Vue Test Utils, testowanie composables i stores Pinia, mockowanie API, pomiar pokrycia oraz pytania zadawane na rozmowach kwalifikacyjnych.

Przepływ pracy testowania Vue z Vitest i Vue Test Utils w 2026

Testowanie Vue oddziela pewne refaktoryzacje od kruchego zgadywania, a w 2026 roku zestaw narzędzi skonsolidował się wokół dwóch bibliotek: Vitest jako runnera oraz Vue Test Utils do montowania komponentów. Ten przewodnik pokazuje, jak skonfigurować stos technologiczny, testować komponenty i composables, mockować zależności oraz odpowiadać na pytania rekrutacyjne o testowanie Vue, które faktycznie padają podczas rozmów.

Nowoczesny zestaw do testowania Vue w 2026

Domyślna rekomendacja z dokumentacji Vue to Vitest do testów jednostkowych i komponentowych, Vue Test Utils jako niskopoziomowe API do montowania oraz Playwright lub Cypress do testów end-to-end. Vitest wykorzystuje tę samą konfigurację Vite co aplikacja, więc testy przechodzą przez identyczny pipeline transformacji z niemal natychmiastowym przeładowaniem na gorąco.

Konfiguracja Vitest dla projektu Vue 3

Vitest współdzieli pipeline Vite, co oznacza, że jeden plik konfiguracyjny obsługuje zarówno serwer deweloperski, jak i runner testów. Opcja environment musi być ustawiona na jsdom lub happy-dom, aby testy komponentów miały DOM, w którym mogą się renderować. Flaga globals: true udostępnia describe, it i expect bez importowania ich w każdym pliku.

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)),
    },
  },
})

W Vitest 3 powyższa konfiguracja działa bez dodatkowych ustawień po zainstalowaniu vitest, @vue/test-utils, @vitejs/plugin-vue oraz jsdom. Ponieważ alias odzwierciedla konfigurację aplikacji, importy takie jak @/composables/useCart rozwiązują się identycznie w testach i na produkcji.

Montowanie komponentów z Vue Test Utils

Vue Test Utils udostępnia dwa punkty wejścia: mount, który renderuje pełne drzewo komponentu, oraz shallowMount, który zastępuje komponenty potomne zaślepkami. W większości testów jednostkowych preferowany jest mount, ponieważ uruchamia rzeczywiste zachowanie renderowania. Zwrócony wrapper udostępnia pomocnicze metody zapytań takie jak find, get i text, które pozwalają weryfikować wyrenderowany rezultat.

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')
  })
})

Wskazywanie elementów poprzez atrybuty data-test zamiast klas CSS sprawia, że testy pozostają odporne: zmiana stylów w .price--sale nie zepsuje selektora, tylko rzeczywista zmiana zachowania to zrobi. To rozdzielenie odpowiedzialności powraca jako stały motyw w łatwym w utrzymaniu testowaniu Vue.

Testowanie zdarzeń użytkownika i emitowanych zdarzeń

Komponenty interaktywne wymagają asercji dotyczących tego, co dzieje się po kliknięciu lub wpisaniu tekstu. Vue Test Utils zwraca z trigger obietnicę, a jej oczekiwanie opróżnia kolejkę reaktywności Vue, dzięki czemu DOM odzwierciedla aktualizację. Pomocnik emitted rejestruje każde niestandardowe zdarzenie wyemitowane przez komponent, co pozwala weryfikować kontrakty między rodzicem a dzieckiem.

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')
  })
})
Zawsze oczekuj na aktualizacje DOM

Vue grupuje aktualizacje reaktywne asynchronicznie. Zapomnienie o await przy wywołaniu trigger lub setValue to najczęstsza przyczyna niestabilnych testów Vue: asercje wykonują się, zanim DOM zostanie ponownie wyrenderowany, przez co odczytują nieaktualny rezultat. Gdy zmiana stanu następuje poza pomocnikiem zdarzeń, należy zaimportować nextTick z vue i wywołać await nextTick() przed asercją.

Testowanie composables Vue w izolacji

Composables, które używają wyłącznie API reaktywności (ref, computed, watch), nie potrzebują żadnego komponentu: są to zwykłe funkcje zwracające reaktywny stan, więc wystarczy wywołać je bezpośrednio i sprawdzać wartość .value. To najszybszy i najbardziej skoncentrowany sposób na pokrycie logiki biznesowej, dobrze współgrający z wzorcami opisanymi w zaawansowanych composables 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)
  })
})

Wyjątkiem są composables zależne od hooków cyklu życia, takich jak onMounted: muszą one działać wewnątrz instancji komponentu. Powszechnym obejściem jest maleńki komponent pomocniczy, który wywołuje composable w setup i udostępnia rezultat, a następnie zamontowanie tego komponentu za pomocą Vue Test Utils.

Gotowy na rozmowy o Vue.js / Nuxt.js?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Mockowanie wywołań API za pomocą vi.mock

Rzeczywiste żądania sieciowe czynią testy wolnymi i niedeterministycznymi. Vitest zastępuje moduły przez vi.mock i tworzy funkcje szpiegujące za pomocą vi.fn. Poniższy wzorzec mockuje moduł opakowujący fetch, dzięki czemu testowany composable otrzymuje kontrolowane dane, nigdy nie odwołując się do serwera.

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')
  })
})

Mockowanie na granicy modułu pozostawia wewnętrzną logikę composable nietkniętą, jednocześnie dając każdemu testowi pełną kontrolę nad odpowiedzią API, w tym nad ścieżkami błędów. Wywołanie mockRejectedValue pozwala pojedynczemu testowi sprawdzić, czy niepowodzenia ustawiają stan błędu i zatrzymują spinner.

Testowanie stores Pinia w 2026

Stores przechowują stan współdzielony między komponentami, a oficjalny pakiet @pinia/testing znacząco upraszcza ich testowanie. createTestingPinia domyślnie zastępuje zaślepką każdą akcję, dzięki czemu test komponentu może sprawdzić, że akcja została wywołana, bez uruchamiania jej efektów ubocznych. Przewodnik po testowaniu Pinia rekomenduje to podejście do testów na poziomie komponentów.

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()
  })
})

Aby testować samą logikę store, a nie integrację z komponentem, należy przekazać stubActions: false, dzięki czemu akcje wykonują się normalnie, a następnie weryfikować wynikowy stan. Ten podział na testy komponentów z zaślepionymi akcjami oraz testy store z rzeczywistymi akcjami sprawia, że każdy zestaw skupia się na jednej odpowiedzialności.

Uruchamianie testów w trybie watch i pomiar pokrycia

Vitest oferuje tryb watch, który ponownie uruchamia tylko testy dotknięte zapisanym plikiem, co skraca pętlę sprzężenia zwrotnego podczas developmentu do ułamka sekundy. Raportowanie pokrycia jest wbudowane poprzez provider @vitest/coverage-v8 i czysto integruje się z pipeline ciągłej integracji, gdzie nieudany test lub regresja pokrycia mogą zablokować scalenie.

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/**'],
    },
  },
})

Samo uruchomienie vitest startuje lokalnie tryb watch, natomiast vitest run --coverage wykonuje pojedyncze przejście dopasowane do CI. Ustawienie jawnych progów zamienia pokrycie z metryki próżności w wymuszaną bramkę jakości: pull request, który obniża pokrycie gałęzi poniżej 75 procent, automatycznie zawodzi, co skłania współtwórców do testowania dodawanych ścieżek, a nie tylko scenariusza pozytywnego.

Najczęstsze pytania rekrutacyjne o testowanie Vue wraz z odpowiedziami

Pytania o testowanie pojawiają się na większości rozmów dla seniorów Vue, ponieważ ujawniają, jak kandydat myśli o łatwości utrzymania. Poniższe odpowiedzi obejmują pojęcia, które padają najczęściej, a pełny zestaw jest przećwiczony w module rekrutacyjnym o testowaniu Vue.

Kiedy używać shallowMount zamiast mount? Po shallowMount warto sięgnąć, gdy komponent renderuje ciężkie lub już przetestowane dzieci, a test dotyczy wyłącznie własnej logiki rodzica. Zastąpienie dzieci zaślepkami izoluje jednostkę i przyspiesza renderowanie, kosztem braku weryfikacji rzeczywistej integracji między komponentami.

Jak obsługuje się aktualizacje asynchroniczne w Vue Test Utils? Vue nakłada zmiany reaktywne w kolejnym ticku. Pomocniki zdarzeń takie jak trigger i setValue zwracają obietnice, które rozwiązują się po aktualizacji DOM, więc ich oczekiwanie wystarcza. Dla stanu zmienianego poza tymi pomocnikami wywołanie await nextTick() wymusza opróżnienie kolejki przed uruchomieniem asercji.

Jaka jest różnica między find a get? find zwraca pusty wrapper, gdy żaden element nie pasuje, i nigdy nie rzuca wyjątku, co pasuje do asercji typu expect(wrapper.find('.error').exists()).toBe(false). get rzuca natychmiast przy brakującym elemencie, co czyni go lepszym wyborem, gdy element powinien istnieć, a jego brak jest rzeczywistym błędem.

Dlaczego preferować atrybuty data-test zamiast selektorów klas? Nazwy klas zmieniają się wraz ze stylami i strukturą, więc testy, które je odpytują, psują się przy kosmetycznych edycjach. Dedykowany hak data-test wyraża intencję testową jawnie i przetrwa refaktoryzacje, ograniczając fałszywe negatywy.

Jak testować composable, który polega na onMounted? Hooki cyklu życia uruchamiają się wyłącznie wewnątrz instancji komponentu, więc composable nie może być wywołany bezpośrednio. Standardowa technika to jednorazowy komponent testowy, który wywołuje composable w setup i udostępnia jego zwracaną wartość, a następnie zamontowanie tego komponentu za pomocą Vue Test Utils i weryfikacja przez wrapper. Zachowuje to nienaruszone powiązania reaktywności i cyklu życia, jednocześnie izolując testowaną logikę. Więcej pytań koncepcyjnych i praktycznych zebrano w przewodniku najważniejsze pytania rekrutacyjne o Vue.js.

Podsumowanie

Niezawodna konfiguracja testowania Vue w 2026 sprowadza się do kilku świadomych decyzji:

  • Uruchamiaj Vitest z environment: 'jsdom' oraz wtyczką Vue, aby testy współdzieliły dokładny pipeline transformacji Vite aplikacji.
  • Preferuj mount zamiast shallowMount, chyba że komponent potomny jest ciężki lub już pokryty gdzie indziej.
  • Zawsze oczekuj na trigger, setValue i nextTick przed asercją, aby wyeliminować niestabilne, zależne od czasu testy.
  • Testuj composables używające wyłącznie API reaktywności, wywołując je bezpośrednio i sprawdzając wartość .value.
  • Mockuj na granicy modułu za pomocą vi.mock, aby zachowanie sieciowe było deterministyczne, a ścieżki błędów testowalne.
  • Używaj createTestingPinia({ createSpy: vi.fn }) do testów komponentów oraz stubActions: false, gdy sprawdzasz logikę store.
  • Odpytuj elementy przez atrybuty data-test, aby testy psuły się tylko przy rzeczywistych zmianach zachowania, nigdy przy stylach.

Opanowanie tych wzorców sprawia, że testowanie staje się narzędziem projektowym, a nie obowiązkiem, wychwytując regresje, zanim trafią na produkcję. Poznaj pełną ścieżkę nauki Vue i Nuxt, aby dalej budować umiejętności gotowe na rozmowę kwalifikacyjną.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#vue
#testing
#vitest
#vue-test-utils
#interview

Udostępnij

Powiązane artykuły