<v/>
{ }
Vue Testing · Vitest · Testing Library · Composables · Pinia
Vue Testing mit Vitest
und Vue Testing Library

Vue Testing ist dann effektiv, wenn Tests das Verhalten aus Nutzersicht prüfen – nicht Implementierungsdetails. Vitest als blitzschneller Test-Runner und Vue Testing Library als nutzerorientiertes Test-Framework sind die Kombination, die robuste Tests erzeugt, die Refactoring überleben.

16 Min. Lesezeit Vitest · @testing-library/vue · Pinia · vi.mock · userEvent Vue 3 · Vite · TypeScript

1. Warum Vue Testing mit Vitest und Testing Library?

Vue Testing war lange Zeit eine Kombination aus Jest und Vue Test Utils – einer bibliothekenorientierten API, die Komponenten-interne Strukturen wie wrapper.vm.$data, wrapper.find('.class-name') und wrapper.trigger('click') testete. Das Problem mit diesem Ansatz: Tests, die Implementierungsdetails testen, brechen bei jedem Refactoring, auch wenn die Funktionalität aus Nutzersicht unverändert bleibt. Eine umbenannte CSS-Klasse lässt einen Test fehlschlagen, obwohl die Komponente korrekt funktioniert. Ein geändertes Daten-Property macht einen Test kaputt, obwohl das DOM identisch gerendert wird.

Vitest und Vue Testing Library setzen einen anderen Ansatz: Tests interagieren mit der Komponente wie ein echter Nutzer – sie suchen Elemente per Rolle, Label oder Text, nicht per CSS-Selektor oder internen Property-Namen. Vitest als Test-Runner integriert sich nativ in Vite und startet Tests in Millisekunden, weil er dieselbe Konfiguration wie der Dev-Server nutzt. Die Kombination aus nutzerorientierter Vue Testing Library-API und der blitzschnellen Ausführung durch Vitest schafft eine Vue Testing-Erfahrung, bei der Tests schnell geschrieben, schnell ausgeführt und langlebig sind.

2. Vitest und Vue Testing Library aufsetzen

Das Setup einer Vue Testing-Umgebung mit Vitest und Vue Testing Library in einem Vite-Projekt ist in wenigen Schritten erledigt. Die Pakete vitest, @testing-library/vue, @testing-library/user-event, jsdom und @vue/test-utils werden als Dev-Abhängigkeiten installiert. In der vite.config.ts wird der test-Schlüssel mit environment: 'jsdom' und globals: true konfiguriert. Mit globals: true sind describe, it, expect, beforeEach und vi global verfügbar ohne explizite Imports in jeder Testdatei – analog zum Jest-Verhalten, das viele Entwickler gewohnt sind.

Die Setup-Datei src/test/setup.ts, die über die setupFiles-Option in Vitest geladen wird, ist der richtige Ort für globale Test-Konfiguration: Pinia-Initialisierung, MSW (Mock Service Worker) für API-Mocking und globale Komponenten-Registrierung. Mit @testing-library/jest-dom erweiterte Matcher wie toBeVisible(), toHaveTextContent() und toBeDisabled() werden ebenfalls in der Setup-Datei eingebunden. TypeScript-Nutzer müssen vitest/globals in das tsconfig.json-types-Array aufnehmen, damit die globalen Test-Funktionen typisiert sind.


// vite.config.ts — Vitest configuration integrated into Vite config
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    // Coverage via v8 or istanbul
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      include: ['src/**/*.{ts,vue}'],
      exclude: ['src/test/**', 'src/**/*.d.ts']
    }
  }
})

// src/test/setup.ts — Global test setup
import '@testing-library/jest-dom'
import { config } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach } from 'vitest'

// Reset Pinia before each test to avoid state leakage
beforeEach(() => {
  setActivePinia(createPinia())
})

// Register global plugins if needed
config.global.plugins = []

3. Erste Component-Tests schreiben

Der erste Vue Testing-Test mit der Vue Testing Library folgt dem Arrange-Act-Assert-Muster: Komponente mit render() mounten, User-Interaktion simulieren, erwartete DOM-Änderungen mit Queries und Assertions prüfen. Die Queries der Vue Testing Library spiegeln wider, wie Nutzer und Screenreader mit der Seite interagieren: getByRole für semantische HTML-Elemente, getByLabelText für Form-Inputs, getByText für sichtbaren Text und getByPlaceholderText als Fallback. Die bevorzugte Query-Hierarchie endet bei getByTestId mit data-testid-Attributen als letztem Ausweg.

Props werden als zweites Argument an render() übergeben: render(MyComponent, { props: { title: 'Test' } }). Global Plugins wie Pinia und Vue Router werden im global-Objekt konfiguriert. Das Ergebnis von render() enthält alle Query-Funktionen der Vue Testing Library, gebunden an den gerenderten Container. Alternativ sind alle Queries auch über das globale screen-Objekt zugänglich – screen.getByRole('button', { name: 'Absenden' }) – was Tests lesbarer macht und keinen Destructuring-Import aus dem Render-Ergebnis erfordert.

4. User Events und async Interaktionen testen

Das Testen von Nutzerinteraktionen ist der Kern des Vue Testing-Ansatzes mit der Vue Testing Library. @testing-library/user-event simuliert echte Browser-Events inklusive aller begleitenden Events: Ein Klick mit userEvent.click(button) löst mousedown, mouseup und click aus – genau wie in einem echten Browser. userEvent.type(input, 'Hallo') tippt Zeichen für Zeichen und löst dabei keydown, keypress, input und keyup für jeden Buchstaben aus. Das ist entscheidend für Komponenten, die auf einzelne Tasten-Events hören – z. B. für Echtzeit-Validierung oder Autocomplete-Felder.

Asynchrone Operationen in Vue Testing erfordern await vor User-Event-Aufrufen, weil Vitest mit @testing-library/user-event v14 alle Events als Promises zurückgibt. Vue's Reaktivitätssystem ist synchron, aber DOM-Updates nach asynchronen Operationen (API-Calls, Timer) benötigen await waitFor(() => ...) oder await screen.findByText('Ergebnis'). findBy*-Queries sind asynchron und warten bis zu einem Timeout auf das Erscheinen des gesuchten Elements – perfekt für Tests, die auf das Ergebnis eines API-Calls warten.


// SearchForm.test.ts — Testing user interaction with userEvent
import { render, screen, waitFor } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import SearchForm from '@/components/SearchForm.vue'
import * as api from '@/api/search'

describe('SearchForm', () => {
  it('calls search API and shows results on submit', async () => {
    // Arrange: mock API response
    const searchSpy = vi.spyOn(api, 'search').mockResolvedValue([
      { id: 1, title: 'Vue.js Guide' },
      { id: 2, title: 'Vitest Tutorial' }
    ])

    const user = userEvent.setup()
    render(SearchForm)

    // Act: type into search field and submit
    await user.type(screen.getByRole('searchbox'), 'Vue')
    await user.click(screen.getByRole('button', { name: /suchen/i }))

    // Assert: results appear in DOM
    await waitFor(() => {
      expect(screen.getByText('Vue.js Guide')).toBeInTheDocument()
      expect(screen.getByText('Vitest Tutorial')).toBeInTheDocument()
    })

    // Verify API was called with correct argument
    expect(searchSpy).toHaveBeenCalledWith('Vue')
  })

  it('shows error message when search fails', async () => {
    vi.spyOn(api, 'search').mockRejectedValue(new Error('Network error'))
    const user = userEvent.setup()
    render(SearchForm)

    await user.type(screen.getByRole('searchbox'), 'test')
    await user.click(screen.getByRole('button', { name: /suchen/i }))

    // findBy* waits for async DOM update
    expect(await screen.findByRole('alert')).toHaveTextContent(/fehler/i)
  })
})

5. Composables isoliert testen

Composables sind in Vue Testing besonders einfach zu testen, wenn sie sauber von Komponenten getrennt sind. Ein Composable, der keine Vue-spezifischen Features außer der Reactivity API nutzt, lässt sich direkt in einem Vitest-Test instantiieren: const { count, increment } = useCounter(). Da ref und computed in Vitest voll funktionsfähig sind, kann man reaktive State-Änderungen testen, indem man Funktionen aufruft und dann .value der Refs prüft.

Composables, die Lifecycle-Hooks wie onMounted, onUnmounted oder watch-Effects nutzen, müssen in einem Vue Testing Library-Komponenten-Kontext ausgeführt werden. Das Hilfsmuster dafür: Eine kleine Wrapper-Komponente erstellen, die den Composable aufruft und eine Ref auf das Ergebnis nach außen gibt, oder withSetup – eine Testhelfer-Funktion, die eine minimale Vue-App startet, den Composable darin ausführt und das Ergebnis zurückgibt. Das ermöglicht das Testen von Lifecycle-Hooks ohne eine vollständige Komponente schreiben zu müssen.

6. Pinia-Stores in Tests integrieren

Pinia-Stores in Vue Testing verhalten sich durch setActivePinia(createPinia()) in der Setup-Datei wie in der echten Anwendung – jeder Test startet mit einem frischen, leeren Store. Für Component-Tests, die einen Store benutzen, muss Pinia als Global-Plugin beim render()-Aufruf übergeben werden: render(MyComponent, { global: { plugins: [createPinia()] } }). Alternativ verwendet man eine renderWithPlugins-Hilfsfunktion, die Pinia, Vue Router und andere globale Plugins automatisch hinzufügt, um Boilerplate in jedem Test zu vermeiden.

Store-State für Tests vorbefüllen: Den Store im Test importieren, nach dem Render aufrufen und State direkt setzen. const authStore = useAuthStore(); authStore.user = { id: 1, name: 'Test' }. Da der Store reaktiv ist, löst diese Zuweisung sofort ein Re-Render der Komponente aus. Das ermöglicht es, Komponenten in verschiedenen Auth-Zuständen zu testen, ohne HTTP-Anfragen simulieren oder komplexe Mock-Strukturen aufbauen zu müssen. Store-Actions lassen sich mit vi.spyOn(authStore, 'logout') überwachen, um zu prüfen, ob die Komponente die richtige Action aufruft.


// UserProfile.test.ts — Testing components with Pinia store
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createPinia, setActivePinia } from 'pinia'
import { vi } from 'vitest'
import UserProfile from '@/components/UserProfile.vue'
import { useAuthStore } from '@/stores/auth'

// Helper: render with Pinia plugin
function renderWithPinia(component: any, options = {}) {
  const pinia = createPinia()
  return render(component, {
    global: { plugins: [pinia] },
    ...options
  })
}

describe('UserProfile', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('shows user name from store', () => {
    const auth = useAuthStore()
    auth.user = { id: 1, name: 'Maria Muster', email: 'maria@example.de' }

    renderWithPinia(UserProfile)

    expect(screen.getByText('Maria Muster')).toBeInTheDocument()
  })

  it('calls logout action on button click', async () => {
    const auth = useAuthStore()
    auth.user = { id: 1, name: 'Test User', email: 'test@example.de' }
    const logoutSpy = vi.spyOn(auth, 'logout').mockResolvedValue()

    const user = userEvent.setup()
    renderWithPinia(UserProfile)

    await user.click(screen.getByRole('button', { name: /abmelden/i }))

    expect(logoutSpy).toHaveBeenCalledOnce()
  })
})

7. HTTP-Requests und Module mocken

HTTP-Requests in Vue Testing zu mocken ist eine Grundvoraussetzung für isolierte, schnelle Tests. Die empfohlene Strategie für Tests mit Vitest: vi.mock('@/api/users') am Dateianfang ersetzt das gesamte Modul durch automatisch generierte Mocks. Einzelne Funktionen werden dann mit vi.spyOn(api, 'fetchUser').mockResolvedValue({ id: 1, name: 'Test' }) konfiguriert. Der Vorteil gegenüber direktem Fetch-/Axios-Mocking: Tests sind unabhängig von der HTTP-Implementierung und brechen nicht, wenn man von Fetch zu Axios wechselt, solange das API-Modul dieselbe Schnittstelle behält.

Für komplexere Szenarien, bei denen viele Tests verschiedene API-Antworten brauchen, ist Mock Service Worker (MSW) die elegantere Lösung. MSW intercepted Requests auf Netzwerk-Ebene, unabhängig davon, ob Fetch, Axios oder ein anderer HTTP-Client genutzt wird. Handler werden deklarativ definiert: rest.get('/api/users', (req, res, ctx) => res(ctx.json([...]))). In der Test-Suite startet man den MSW-Server mit server.listen() in beforeAll und setzt ihn mit server.resetHandlers() in afterEach zurück. Das ermöglicht das Definieren von Test-spezifischen Handler-Overrides, die nur für einen einzelnen Test gelten.

8. Vue Router in Component-Tests

Komponenten, die useRoute(), useRouter() oder <router-link> nutzen, benötigen in Vue Testing einen konfigurierten Router. Das einfachste Muster: einen echten Router mit Speicher-History (createMemoryHistory) erstellen und ihn als Global-Plugin übergeben. Memory History funktioniert ohne Browser-URL-API und ist daher in der jsdom-Testumgebung von Vitest vollständig nutzbar. Die Startroute lässt sich mit router.push('/some-path') vor dem render()-Aufruf setzen – wichtig bei Komponenten, die Route-Params oder Query-String-Werte auswerten.

Für Tests, die nur prüfen wollen, ob eine Komponente korrekt auf Route-Params reagiert, ist das Props-Muster von Vue Router besonders nützlich. Wenn die Route mit props: true konfiguriert ist, lassen sich Route-Params als normale Props an render() übergeben, ohne einen Router instanziieren zu müssen. Das macht den Test schlanker und schneller. router-link-Komponenten, die in Tests nicht navigieren sollen, lassen sich mit einem Stub ersetzen: stubs: { RouterLink: RouterLinkStub } aus @vue/test-utils.

9. Testing-Strategien im Vergleich

Die Wahl der richtigen Vue Testing-Strategie beeinflusst, wie wartbar und stabil die Test-Suite langfristig bleibt. Die folgende Tabelle vergleicht die wichtigsten Ansätze:

Strategie Vue Test Utils allein Vue Testing Library Empfehlung
Element-Suche CSS-Selektoren, .vm-Properties Role, Label, Text Testing Library: refactoring-sicher
Events wrapper.trigger('click') userEvent.click() userEvent: echte Event-Kaskade
Async warten await nextTick() waitFor, findBy* findBy*: selbst-retrying bis Timeout
API-Mocking Fetch/Axios global überschreiben vi.mock oder MSW MSW: netzwerkebene, HTTP-Impl-agnostisch
Composables Wrapper-Komponente manuell withSetup-Helper oder direkt Direkt wenn keine Lifecycle-Hooks

Die Vue Testing Library-Philosophie – Tests schreiben, wie Nutzer die Anwendung benutzen – produziert Tests, die auch nach umfangreichem Refactoring noch grün bleiben, weil sie nicht an interne Implementierungsdetails gebunden sind. Dieser Ansatz kostet anfangs mehr Gedanken bei der Test-Formulierung, zahlt sich aber in größeren Projekten durch drastisch reduzierten Test-Maintenance-Aufwand aus.

Mironsoft

Vue.js Testing, Qualitätssicherung und CI-Integration

Vue Testing-Strategie für euer Projekt aufbauen?

Wir helfen beim Aufbau einer robusten Test-Suite mit Vitest und Vue Testing Library – inklusive Composable-Tests, Pinia-Integration und CI-Pipeline-Konfiguration.

Test-Audit

Bestehende Tests auf Wartbarkeit, Coverage und Implementierungsdetail-Abhängigkeiten prüfen

Vitest-Setup

Vitest, Vue Testing Library und MSW konfigurieren – mit TypeScript-Support und Coverage-Reports

CI-Integration

Vitest in GitHub Actions oder GitLab CI integrieren – mit automatischen Coverage-Reports und Failing-PR-Checks

10. Zusammenfassung

Vue Testing mit Vitest und Vue Testing Library folgt einer klaren Philosophie: Tests sollen das Verhalten aus Nutzersicht prüfen, nicht Implementierungsdetails. getByRole, getByText und getByLabelText finden Elemente wie ein Nutzer sie findet. userEvent simuliert echte Browser-Interaktionen mit der vollständigen Event-Kaskade. waitFor und findBy*-Queries warten auf asynchrone DOM-Änderungen ohne künstliche Timeouts.

Pinia-Stores in Tests zu isolieren ist durch setActivePinia(createPinia()) in der Setup-Datei trivial. Composables ohne Lifecycle-Hooks lassen sich direkt in Vitest testen. HTTP-Mocking mit vi.mock oder MSW hält Tests schnell und von Netzwerkzustand unabhängig. Das Ergebnis: Eine Test-Suite, die bei Refactorings stabil bleibt, schnell läuft und echte Bugs findet, bevor sie in Produktion gehen.

Vue Testing mit Vitest und Vue Testing Library — Das Wichtigste

Nutzerorientierte Queries

getByRole, getByText, getByLabelText – kein CSS-Selektor, keine internen Properties. Tests überleben Refactorings unverändert.

userEvent statt trigger

userEvent.click() und userEvent.type() simulieren echte Browser-Events inklusive der vollständigen Event-Kaskade – analog zum echten Nutzerverhalten.

Pinia und Router isolieren

Frische Pinia-Instanz pro Test via setActivePinia(createPinia()). Router mit createMemoryHistory – kein Browser-URL-API nötig in jsdom.

API-Mocking

vi.mock für einfache Module, MSW für netzwerkebene HTTP-Interception – HTTP-Client-Implementierung austauschbar ohne Test-Anpassung.

11. FAQ: Vue Testing mit Vitest und Vue Testing Library

1Vitest vs. Jest für Vue-Projekte?
Vitest nutzt die Vite-Konfiguration direkt und startet in Millisekunden – keine separate Babel/SWC-Pipeline. In Vite-Projekten ist Vitest die natürliche, konfigurationsarme Wahl.
2Warum Vue Testing Library statt Vue Test Utils?
Testing Library sucht per Rolle, Label und Text statt CSS-Selektor – Tests brechen nicht bei Refactorings ohne Funktionsänderung. Beide Bibliotheken können kombiniert werden.
3Composables mit Lifecycle-Hooks testen?
withSetup-Helper startet eine minimale Vue-App und führt den Composable darin aus – simuliert onMounted und onUnmounted korrekt.
4State-Leckagen zwischen Tests vermeiden?
setActivePinia(createPinia()) in beforeEach – jeder Test bekommt eine frische Pinia-Instanz ohne State aus vorherigen Tests.
5MSW vs. vi.mock – wann was?
MSW für HTTP-Interception auf Netzwerkebene (HTTP-Client-agnostisch). vi.mock für ganze Module oder wenn keine echten HTTP-Calls stattfinden.
6Komponenten mit Vue Router testen?
Echten Router mit createMemoryHistory als Global-Plugin übergeben. router.push() vor dem Render für die Startroute – funktioniert in jsdom ohne Browser-URL-API.
7getBy vs. findBy in Vue Testing Library?
getBy* synchron, wirft sofort. findBy* asynchron, retried bis Timeout – ideal nach API-Calls und anderen asynchronen DOM-Updates.
8Code-Coverage in Vitest konfigurieren?
test.coverage.provider: 'v8' in vite.config.ts, reporter auf ['text', 'html', 'lcov'] setzen. Starten mit vitest run --coverage.
9userEvent für Tastenkombinationen?
userEvent.keyboard('{Enter}'), {Escape}, {ctrl>}a{/ctrl} für Modifier-Kombinationen. Vollständige Keyboard-Simulation wie in einem echten Browser.
10Navigation Guards in Tests prüfen?
Router mit echten Guards instanziieren, router.push() zur geschützten Route, dann await router.isReady() und router.currentRoute.value.name prüfen.