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.
Inhaltsverzeichnis
- 1. Warum Vue Testing mit Vitest und Testing Library?
- 2. Vitest und Vue Testing Library aufsetzen
- 3. Erste Component-Tests schreiben
- 4. User Events und async Interaktionen testen
- 5. Composables isoliert testen
- 6. Pinia-Stores in Tests integrieren
- 7. HTTP-Requests und Module mocken
- 8. Vue Router in Component-Tests
- 9. Testing-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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?
2Warum Vue Testing Library statt Vue Test Utils?
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?
6Komponenten mit Vue Router testen?
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.push() zur geschützten Route, dann await router.isReady() und router.currentRoute.value.name prüfen.