Vue 3 testen in 2026: Vitest, Vue Test Utils en interviewvragen

Een praktische gids voor het testen van Vue in 2026: Vitest configureren, componenten mounten met Vue Test Utils, composables en Pinia-stores testen, API's mocken, coverage meten en de interviewvragen die teams stellen.

Vue-testworkflow met Vitest en Vue Test Utils in 2026

Testen scheidt zelfverzekerde refactors van broze giswerk, en in 2026 heeft de Vue-toolchain zich rond twee bibliotheken gevormd: Vitest als testrunner en Vue Test Utils om componenten te mounten. Deze gids behandelt het opzetten van de stack, het testen van componenten en composables, het mocken van afhankelijkheden en het beantwoorden van de Vue-interviewvragen die teams echt stellen.

De moderne Vue-teststack in 2026

De standaardaanbeveling uit de Vue-documentatie is Vitest voor unit- en componenttests, Vue Test Utils als low-level mounting-API en Playwright of Cypress voor end-to-end-dekking. Vitest hergebruikt dezelfde Vite-configuratie als de applicatie, waardoor tests door exact dezelfde transformpijplijn lopen met vrijwel directe hot reload.

Vitest configureren voor een Vue 3-project

Vitest deelt de Vite-pijplijn, wat betekent dat één configuratiebestand zowel de dev-server als de testrunner aanstuurt. De optie environment moet op jsdom of happy-dom staan, zodat componenttests een DOM hebben om in te renderen. De vlag globals: true stelt describe, it en expect beschikbaar zonder ze in elk bestand te importeren.

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

Met Vitest 3 werkt bovenstaande configuratie meteen na het installeren van vitest, @vue/test-utils, @vitejs/plugin-vue en jsdom. Doordat de alias de applicatieconfiguratie spiegelt, worden imports als @/composables/useCart in tests en productie op identieke wijze opgelost.

Componenten mounten met Vue Test Utils

Vue Test Utils biedt twee ingangen: mount, dat de volledige componentboom rendert, en shallowMount, dat kindcomponenten stubt. Voor de meeste unittests verdient mount de voorkeur, omdat het echte renderlogica uitvoert. De teruggegeven wrapper levert query-helpers als find, get en text om assertions op de gerenderde output te doen.

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

Elementen aanspreken via data-test-attributen in plaats van CSS-klassen houdt tests robuust: een stijlwijziging aan .price--sale breekt de selector niet, alleen een echte gedragsverandering doet dat. Deze ontkoppeling is een terugkerend thema in onderhoudbare Vue-tests.

Gebruikersinteracties en emitted events testen

Interactieve componenten vereisen assertions op wat er na een klik of invoer gebeurt. Vue Test Utils geeft een promise terug van trigger, en door daarop te awaiten wordt de reactiviteitswachtrij van Vue geleegd, zodat de DOM de update weerspiegelt. De helper emitted registreert elke custom event die een component uitstuurt, en zo worden de contracten tussen ouder en kind geverifieerd.

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')
  })
})
Await altijd de DOM-updates

Vue verwerkt reactieve updates asynchroon in batches. Vergeten te await-en op een trigger- of setValue-aanroep is de meest voorkomende oorzaak van flaky Vue-tests: de assertions draaien voordat de DOM opnieuw rendert, waardoor ze verouderde output lezen. Wanneer een statuswijziging buiten een event-helper plaatsvindt, importeer dan nextTick uit vue en gebruik await nextTick() vóór de assertie.

Vue-composables geïsoleerd testen

Composables die enkel reactiviteits-API's gebruiken (ref, computed, watch) hebben helemaal geen component nodig: het zijn gewone functies die reactieve status teruggeven, dus ze rechtstreeks aanroepen en op .value asserten is voldoende. Dit is de snelste en meest gerichte manier om bedrijfslogica te dekken, en het sluit goed aan bij de patronen uit geavanceerde Vue 3-composables.

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

Composables die afhangen van lifecycle-hooks zoals onMounted vormen de uitzondering: die moeten binnen een component-instantie draaien. De gebruikelijke oplossing is een minuscuul hulpcomponent dat de composable in setup aanroept en het resultaat blootstelt, waarna dat hulpcomponent met Vue Test Utils gemount wordt.

Klaar om je Vue.js / Nuxt.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

API-aanroepen mocken met vi.mock

Echte netwerkverzoeken maken tests traag en niet-deterministisch. Vitest vervangt modules met vi.mock en maakt spy-functies aan met vi.fn. Het onderstaande patroon mockt de module die fetch omhult, zodat de composable die getest wordt gecontroleerde data ontvangt zonder ooit een server te raken.

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

Mocken op de modulegrens laat de interne logica van de composable ongemoeid, terwijl elke test volledige controle krijgt over het API-antwoord, inclusief foutpaden. Door in plaats daarvan mockRejectedValue aan te roepen kan één test verifiëren dat fouten een errorstatus zetten en de spinner stoppen.

Pinia-stores testen in 2026

Stores bevatten status die over componenten heen gedeeld wordt, en het officiële pakket @pinia/testing maakt ze eenvoudig te testen. createTestingPinia stubt standaard elke action, zodat een componenttest kan asserten dat een action werd gedispatcht zonder de bijwerkingen ervan uit te voeren. De Pinia-testgids beveelt deze aanpak aan voor tests op componentniveau.

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

Om de storelogica zelf te testen in plaats van de integratie met een component, geef je stubActions: false mee, zodat de actions normaal draaien, en assert je vervolgens op de resulterende status. Deze scheiding tussen componenttests met gestubte actions en storetests met echte actions houdt elke suite gericht op één verantwoordelijkheid.

Tests draaien in watch-modus en coverage meten

Vitest levert een watch-modus die alleen de tests herdraait die door een opgeslagen bestand geraakt worden, wat de feedbackloop tijdens de ontwikkeling terugbrengt tot een fractie van een seconde. Coveragerapportage zit ingebouwd via de provider @vitest/coverage-v8 en integreert soepel in een continuous-integration-pijplijn, waar een falende test of een coverageregressie een merge kan blokkeren.

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 op zichzelf starten activeert lokaal de watch-modus, terwijl vitest run --coverage een enkele doorloop uitvoert die geschikt is voor CI. Expliciete thresholds instellen maakt van coverage geen ijdele metriek maar een afgedwongen kwaliteitspoort: een pull request die de branch-coverage onder 75 procent laat zakken, faalt automatisch, wat bijdragers ertoe aanzet ook de paden te testen die ze toevoegen in plaats van alleen het happy path.

Veelgestelde Vue-interviewvragen over testen, met antwoorden

Vragen over testen duiken op in de meeste senior Vue-interviews, omdat ze onthullen hoe een kandidaat over onderhoudbaarheid nadenkt. De onderstaande antwoorden behandelen de concepten die het vaakst voorbijkomen, en de volledige set wordt geoefend in de Vue-testing-interviewmodule.

Wanneer gebruik je shallowMount in plaats van mount? Grijp naar shallowMount wanneer een component zware of al geteste kinderen rendert en de test alleen om de eigen logica van de ouder geeft. Kinderen stubben isoleert de unit en versnelt het renderen, ten koste van het niet verifiëren van echte integratie tussen componenten.

Hoe worden asynchrone updates afgehandeld in Vue Test Utils? Vue past reactieve wijzigingen toe op de volgende tick. Event-helpers als trigger en setValue geven promises terug die pas resolven nadat de DOM is bijgewerkt, dus daarop awaiten volstaat. Voor status die buiten die helpers wijzigt, forceert await nextTick() het legen van de wachtrij voordat de assertions draaien.

Wat is het verschil tussen find en get? find geeft een lege wrapper terug wanneer geen enkel element matcht en werpt nooit een fout, wat past bij assertions als expect(wrapper.find('.error').exists()).toBe(false). get werpt onmiddellijk een fout bij een ontbrekend element, wat het de betere keuze maakt wanneer het element verwacht wordt en de afwezigheid ervan een echte fout is.

Waarom data-test-attributen verkiezen boven klasse-selectors? Klassenamen veranderen met styling en structuur, dus tests die ze bevragen breken bij cosmetische aanpassingen. Een aparte data-test-hook drukt de testintentie expliciet uit en overleeft refactors, wat valse negatieven vermindert.

Hoe test je een composable die op onMounted steunt? Lifecycle-hooks vuren alleen binnen een component-instantie, dus de composable kan niet rechtstreeks worden aangeroepen. De standaardtechniek is een wegwerp-harnescomponent dat de composable in setup aanroept en de returnwaarde blootstelt, waarna je dat harnas met Vue Test Utils mount en via de wrapper assert. Zo blijft de reactiviteits- en lifecycle-bedrading intact terwijl de te testen logica toch geïsoleerd blijft. Meer conceptuele en coding-vragen zijn verzameld in de gids essentiële Vue.js-interviewvragen.

Conclusie

Een betrouwbare Vue-testopzet in 2026 komt neer op een handvol bewuste keuzes:

  • Draai Vitest met environment: 'jsdom' en de Vue-plugin, zodat tests exact dezelfde Vite-transformpijplijn delen als de applicatie.
  • Verkies mount boven shallowMount, tenzij een kindcomponent zwaar is of elders al gedekt wordt.
  • Await altijd trigger, setValue en nextTick voor de assertions om flaky, timingafhankelijke tests uit te sluiten.
  • Test composables die enkel reactiviteits-API's gebruiken door ze rechtstreeks aan te roepen en op .value te asserten.
  • Mock op de modulegrens met vi.mock, zodat netwerkgedrag deterministisch is en foutpaden testbaar zijn.
  • Gebruik createTestingPinia({ createSpy: vi.fn }) voor componenttests en stubActions: false bij het testen van storelogica.
  • Bevraag elementen via data-test-attributen, zodat tests alleen breken op echte gedragsveranderingen en nooit op styling.

Beheers deze patronen en testen wordt een ontwerpinstrument in plaats van een karwei, dat regressies opvangt voordat ze productie bereiken. Verken het volledige Vue- en Nuxt-leertraject om je interviewklare vaardigheden verder uit te bouwen.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

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

Delen

Gerelateerde artikelen