Vue-3-Testing 2026: Vitest, Vue Test Utils und Interviewfragen

Ein praxisnaher Leitfaden zum Vue-Testing 2026: Vitest konfigurieren, Komponenten mit Vue Test Utils mounten, Composables und Pinia-Stores testen, APIs mocken, Coverage messen und die Interviewfragen, die Recruiting-Teams stellen.

Vue-Testing-Workflow mit Vitest und Vue Test Utils im Jahr 2026

Vue-Testing trennt selbstbewusste Refactorings von fragilem Rätselraten, und 2026 hat sich die Toolchain auf zwei Bibliotheken verdichtet: Vitest als Testrunner und Vue Test Utils zum Mounten von Komponenten. Dieser Leitfaden zeigt, wie sich der Stack konfigurieren lässt, wie Komponenten und Composables getestet werden, wie Abhängigkeiten gemockt werden und wie sich die Vue-Testing-Interviewfragen beantworten lassen, die Recruiting-Teams tatsächlich stellen.

Der moderne Vue-Testing-Stack 2026

Die Standardempfehlung aus der Vue-Dokumentation lautet: Vitest für Unit- und Komponententests, Vue Test Utils als Low-Level-Mounting-API und Playwright oder Cypress für End-to-End-Abdeckung. Vitest verwendet dieselbe Vite-Konfiguration wie die Anwendung, sodass Tests durch dieselbe Transform-Pipeline laufen und Änderungen nahezu ohne Verzögerung neu geladen werden.

Vitest für ein Vue-3-Projekt konfigurieren

Vitest teilt sich die Vite-Pipeline, was bedeutet, dass eine einzige Konfigurationsdatei sowohl den Dev-Server als auch den Testrunner steuert. Die Option environment muss auf jsdom oder happy-dom gesetzt werden, damit Komponententests ein DOM zum Rendern haben. Das Flag globals: true stellt describe, it und expect bereit, ohne dass sie in jeder Datei importiert werden müssen.

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

Mit Vitest 3 funktioniert die obige Konfiguration sofort, nachdem vitest, @vue/test-utils, @vitejs/plugin-vue und jsdom installiert wurden. Da der Alias die Anwendungskonfiguration spiegelt, werden Importe wie @/composables/useCart in Tests und Produktion identisch aufgelöst.

Komponenten mit Vue Test Utils mounten

Vue Test Utils bietet zwei Einstiegspunkte: mount, das den vollständigen Komponentenbaum rendert, und shallowMount, das Kindkomponenten stubbt. Für die meisten Unit-Tests ist mount vorzuziehen, weil es das echte Renderverhalten prüft. Der zurückgegebene wrapper stellt Abfrage-Helfer wie find, get und text bereit, um gegen die gerenderte Ausgabe zu prüfen.

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

Elemente über data-test-Attribute statt über CSS-Klassen anzusteuern hält Tests widerstandsfähig: Eine Styling-Änderung an .price--sale bricht den Selektor nicht, nur eine echte Verhaltensänderung tut das. Diese Entkopplung ist ein wiederkehrendes Motiv in wartbarem Vue-Testing.

Benutzeraktionen und ausgelöste Events testen

Interaktive Komponenten brauchen Assertions darüber, was nach einem Klick oder einer Eingabe passiert. Vue Test Utils gibt aus trigger ein Promise zurück, und das Abwarten davon leert die Reaktivitäts-Queue von Vue, sodass das DOM die Aktualisierung widerspiegelt. Der Helfer emitted protokolliert jedes benutzerdefinierte Event, das eine Komponente ausgelöst hat, und genau so werden die Verträge zwischen Eltern- und Kindkomponente überprüft.

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-Aktualisierungen immer abwarten

Vue verarbeitet reaktive Aktualisierungen asynchron in Batches. Ein vergessenes await bei einem trigger- oder setValue-Aufruf ist die häufigste Ursache für flaky Vue-Tests: Die Assertions laufen, bevor das DOM neu gerendert wurde, und lesen deshalb veraltete Ausgabe. Wenn eine Zustandsänderung außerhalb eines Event-Helfers geschieht, sollte nextTick aus vue importiert und vor der Assertion await nextTick() aufgerufen werden.

Vue-Composables isoliert testen

Composables, die nur Reaktivitäts-APIs verwenden (ref, computed, watch), brauchen überhaupt keine Komponente: Es sind schlichte Funktionen, die reaktiven Zustand zurückgeben, sodass ein direkter Aufruf und eine Assertion auf .value ausreichen. Das ist der schnellste und fokussierteste Weg, Geschäftslogik abzudecken, und passt gut zu den Mustern in fortgeschrittenen 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 von Lifecycle-Hooks wie onMounted abhängen, sind die Ausnahme: Sie müssen innerhalb einer Komponenteninstanz laufen. Der übliche Ausweg ist eine winzige Helfer-Komponente, die das Composable in setup aufruft und das Ergebnis bereitstellt, und die dann mit Vue Test Utils gemountet wird.

Bereit für deine Vue.js / Nuxt.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

API-Aufrufe mit vi.mock mocken

Echte Netzwerkanfragen machen Tests langsam und nicht-deterministisch. Vitest ersetzt Module mit vi.mock und erzeugt Spy-Funktionen mit vi.fn. Das folgende Muster mockt das Modul, das fetch kapselt, sodass das getestete Composable kontrollierte Daten erhält, ohne jemals einen Server zu treffen.

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

Das Mocken an der Modulgrenze lässt die interne Logik des Composables unangetastet und gibt jedem Test die volle Kontrolle über die API-Antwort, einschließlich der Fehlerpfade. Ein mockRejectedValue an dieser Stelle ermöglicht es einem einzelnen Test, zu überprüfen, dass Fehler einen Fehlerzustand setzen und den Spinner stoppen.

Pinia-Stores 2026 testen

Stores halten komponentenübergreifenden Zustand, und das offizielle Paket @pinia/testing macht sie unkompliziert testbar. createTestingPinia stubbt standardmäßig jede Action, sodass ein Komponententest prüfen kann, dass eine Action ausgelöst wurde, ohne deren Seiteneffekte auszuführen. Der Pinia-Testing-Leitfaden empfiehlt diesen Ansatz für Tests auf Komponentenebene.

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

Um die Store-Logik selbst statt der Komponentenintegration zu testen, sollte stubActions: false übergeben werden, damit Actions normal ausgeführt werden, um anschließend gegen den resultierenden Zustand zu prüfen. Diese Trennung zwischen Komponententests mit gestubbten Actions und Store-Tests mit echten Actions hält jede Testsuite auf eine einzige Verantwortung fokussiert.

Tests im Watch-Modus ausführen und Coverage messen

Vitest bringt einen Watch-Modus mit, der nur die Tests erneut ausführt, die von einer gespeicherten Datei betroffen sind, was die Feedback-Schleife während der Entwicklung auf einen Bruchteil einer Sekunde verkürzt. Das Coverage-Reporting ist über den Provider @vitest/coverage-v8 eingebaut und integriert sich sauber in eine Continuous-Integration-Pipeline, in der ein fehlschlagender Test oder eine Coverage-Regression einen Merge blockieren kann.

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

Ein blankes vitest startet lokal den Watch-Modus, während vitest run --coverage einen einzelnen Durchlauf ausführt, der für CI geeignet ist. Explizite Schwellenwerte machen aus Coverage statt einer Vanity-Metrik ein erzwungenes Qualitätsgate: Ein Pull Request, der die Branch-Coverage unter 75 Prozent drückt, schlägt automatisch fehl, was Beitragende dazu bewegt, die Pfade zu testen, die sie hinzufügen, statt nur den Happy Path.

Häufige Vue-Testing-Interviewfragen und Antworten

Testing-Fragen tauchen in den meisten Senior-Vue-Interviews auf, weil sie zeigen, wie ein Kandidat über Wartbarkeit denkt. Die folgenden Antworten decken die Konzepte ab, die am häufigsten vorkommen, und der vollständige Satz wird im Vue-Testing-Interviewmodul geübt.

Wann sollte shallowMount statt mount verwendet werden? Zu shallowMount sollte gegriffen werden, wenn eine Komponente schwere oder bereits getestete Kinder rendert und der Test sich nur für die eigene Logik der Elternkomponente interessiert. Das Stubben der Kinder isoliert die Unit und beschleunigt das Rendern, allerdings um den Preis, dass die echte Integration zwischen den Komponenten nicht überprüft wird.

Wie werden asynchrone Aktualisierungen in Vue Test Utils behandelt? Vue wendet reaktive Änderungen im nächsten Tick an. Event-Helfer wie trigger und setValue geben Promises zurück, die nach der DOM-Aktualisierung auflösen, sodass ihr Abwarten genügt. Bei Zuständen, die außerhalb dieser Helfer geändert werden, erzwingt await nextTick() das Leeren der Queue, bevor die Assertions laufen.

Worin unterscheiden sich find und get? find gibt einen leeren Wrapper zurück, wenn kein Element passt, und wirft nie einen Fehler, was zu Assertions wie expect(wrapper.find('.error').exists()).toBe(false) passt. get wirft sofort einen Fehler bei einem fehlenden Element und ist damit die bessere Wahl, wenn das Element erwartet wird und sein Fehlen ein echter Fehler ist.

Warum sind data-test-Attribute Klassenselektoren vorzuziehen? Klassennamen ändern sich mit Styling und Struktur, sodass Tests, die sie abfragen, bei kosmetischen Änderungen brechen. Ein dedizierter data-test-Hook drückt die Test-Absicht explizit aus und übersteht Refactorings, was falsch negative Ergebnisse reduziert.

Wie wird ein Composable getestet, das sich auf onMounted stützt? Lifecycle-Hooks feuern nur innerhalb einer Komponenteninstanz, sodass das Composable nicht direkt aufgerufen werden kann. Die Standardtechnik ist eine Wegwerf-Harness-Komponente, die das Composable in setup aufruft und dessen Rückgabewert bereitstellt, um diese Harness dann mit Vue Test Utils zu mounten und über den Wrapper zu prüfen. So bleibt die Verdrahtung von Reaktivität und Lifecycle intakt, während die getestete Logik trotzdem isoliert wird. Weitere konzeptionelle und Coding-Fragen sind im Leitfaden zu den essenziellen Vue.js-Interviewfragen gesammelt.

Fazit

Ein verlässliches Vue-Testing-Setup läuft 2026 auf wenige bewusste Entscheidungen hinaus:

  • Vitest mit environment: 'jsdom' und dem Vue-Plugin betreiben, damit Tests die exakte Vite-Transform-Pipeline der Anwendung teilen.
  • mount gegenüber shallowMount bevorzugen, außer eine Kindkomponente ist schwer oder anderswo bereits abgedeckt.
  • trigger, setValue und nextTick vor der Assertion immer awaiten, um flaky, zeitabhängige Tests zu vermeiden.
  • Composables, die nur Reaktivitäts-APIs verwenden, durch direkten Aufruf und eine Assertion auf .value testen.
  • An der Modulgrenze mit vi.mock mocken, damit das Netzwerkverhalten deterministisch ist und Fehlerpfade testbar sind.
  • createTestingPinia({ createSpy: vi.fn }) für Komponententests nutzen und stubActions: false, wenn Store-Logik geprüft wird.
  • Elemente über data-test-Attribute abfragen, damit Tests nur bei echten Verhaltensänderungen brechen, nie beim Styling.

Wer diese Muster beherrscht, macht aus Testing ein Design-Werkzeug statt einer lästigen Pflicht und fängt Regressionen ab, bevor sie in die Produktion gelangen. Der vollständige Vue- und Nuxt-Lernpfad hilft dabei, weiter interviewreife Fähigkeiten aufzubauen.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

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

Teilen

Verwandte Artikel