Testing mit Vitest
Vitest ist ein schnelles Unit-Test-Framework, das nativ mit Vite funktioniert. Es bietet eine Jest-kompatible API mit deutlich besserer Performance.
Vorteile:
- Schnell - Native ESM, HMR
- Jest-kompatibel - Bekannte API
- Vue-Integration - @vue/test-utils
Installation
Abschnitt betitelt „Installation“npm install -D vitest @vue/test-utils happy-domimport { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()], test: { environment: 'happy-dom', // oder 'jsdom' globals: true // describe, it, expect global }}){ "scripts": { "test": "vitest", "test:run": "vitest run", "coverage": "vitest run --coverage" }}Grundstruktur eines Tests
Abschnitt betitelt „Grundstruktur eines Tests“import { describe, it, expect } from 'vitest'import { add, multiply } from './math'
describe('math utilities', () => { describe('add', () => { it('adds two positive numbers', () => { expect(add(2, 3)).toBe(5) })
it('handles negative numbers', () => { expect(add(-1, 1)).toBe(0) }) })
describe('multiply', () => { it('multiplies two numbers', () => { expect(multiply(3, 4)).toBe(12) }) })})Assertions (Matchers)
Abschnitt betitelt „Assertions (Matchers)“// Gleichheitexpect(value).toBe(5) // ===expect(obj).toEqual({ a: 1 }) // Deep equalexpect(value).toStrictEqual(x) // Strict deep
// Wahrheitswerteexpect(value).toBeTruthy()expect(value).toBeFalsy()expect(value).toBeNull()expect(value).toBeDefined()
// Zahlenexpect(num).toBeGreaterThan(3)expect(num).toBeLessThanOrEqual(5)expect(0.1 + 0.2).toBeCloseTo(0.3)// Stringsexpect(str).toContain('hello')expect(str).toMatch(/pattern/)
// Arraysexpect(arr).toContain('item')expect(arr).toHaveLength(3)
// Objekteexpect(obj).toHaveProperty('key')expect(obj).toMatchObject({ a: 1 })
// Exceptionsexpect(() => fn()).toThrow()expect(() => fn()).toThrowError('msg')
// Negationexpect(value).not.toBe(5)Vue Komponenten testen
Abschnitt betitelt „Vue Komponenten testen“import { describe, it, expect } from 'vitest'import { mount } from '@vue/test-utils'import Counter from './Counter.vue'
describe('Counter', () => { it('renders initial count', () => { const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0') })
it('increments count on click', async () => { const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1') })})mount vs shallowMount
Abschnitt betitelt „mount vs shallowMount“import { mount } from '@vue/test-utils'
const wrapper = mount(ParentComponent)- Rendert alle Kind-Komponenten
- Für Integrationstests
- Langsamer
import { shallowMount } from '@vue/test-utils'
const wrapper = shallowMount(ParentComponent)- Stubbt Kind-Komponenten
- Für Unit-Tests
- Schneller
Elemente finden
Abschnitt betitelt „Elemente finden“const wrapper = mount(MyComponent)
// Per CSS-Selektorwrapper.find('button') // Erstes Elementwrapper.findAll('li') // Alle Elementewrapper.find('.submit-btn') // Per Klassewrapper.find('#user-input') // Per IDwrapper.find('[data-test="submit"]') // Per data-Attribut (Empfohlen!)
// Per Komponenteimport ChildComponent from './ChildComponent.vue'wrapper.findComponent(ChildComponent)wrapper.findAllComponents(ChildComponent)
// Prüfungenwrapper.find('button').exists() // true/falsewrapper.find('.error').isVisible() // SichtbarkeitProps testen
Abschnitt betitelt „Props testen“// GreetingComponent.vue: <p>Hallo, {{ name }}!</p>
import { mount } from '@vue/test-utils'import Greeting from './Greeting.vue'
describe('Greeting', () => { it('renders name from props', () => { const wrapper = mount(Greeting, { props: { name: 'Max' } })
expect(wrapper.text()).toContain('Hallo, Max!') })
it('updates when props change', async () => { const wrapper = mount(Greeting, { props: { name: 'Max' } })
await wrapper.setProps({ name: 'Anna' })
expect(wrapper.text()).toContain('Hallo, Anna!') })})Events testen
Abschnitt betitelt „Events testen“// ButtonComponent.vue: emits 'click' with payload
import { mount } from '@vue/test-utils'import Button from './Button.vue'
describe('Button', () => { it('emits click event', async () => { const wrapper = mount(Button)
await wrapper.find('button').trigger('click')
// Emitted Events prüfen expect(wrapper.emitted()).toHaveProperty('click') expect(wrapper.emitted('click')).toHaveLength(1) })
it('emits click with payload', async () => { const wrapper = mount(Button, { props: { id: 42 } })
await wrapper.find('button').trigger('click')
// Payload prüfen expect(wrapper.emitted('click')[0]).toEqual([42]) })})Formulare testen
Abschnitt betitelt „Formulare testen“import { mount } from '@vue/test-utils'import LoginForm from './LoginForm.vue'
describe('LoginForm', () => { it('submits form with credentials', async () => { const wrapper = mount(LoginForm)
// Input-Werte setzen await wrapper.find('[data-test="email"]').setValue('test@example.com') await wrapper.find('[data-test="password"]').setValue('secret123')
// Formular absenden await wrapper.find('form').trigger('submit')
// Emittierte Daten prüfen expect(wrapper.emitted('submit')[0][0]).toEqual({ email: 'test@example.com', password: 'secret123' }) })})Mocking: vi.fn() und vi.spyOn()
Abschnitt betitelt „Mocking: vi.fn() und vi.spyOn()“import { describe, it, expect, vi } from 'vitest'
describe('Mocking', () => { it('creates mock function', () => { const mockFn = vi.fn() mockFn('arg1', 'arg2')
expect(mockFn).toHaveBeenCalled() expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2') expect(mockFn).toHaveBeenCalledTimes(1) })
it('mocks return value', () => { const mockFn = vi.fn().mockReturnValue(42) expect(mockFn()).toBe(42) })
it('spies on object method', () => { const obj = { method: () => 'original' } const spy = vi.spyOn(obj, 'method').mockReturnValue('mocked')
expect(obj.method()).toBe('mocked') expect(spy).toHaveBeenCalled() })})Module mocken
Abschnitt betitelt „Module mocken“// api.js: export async function fetchUser(id) { ... }
import { describe, it, expect, vi } from 'vitest'import { mount, flushPromises } from '@vue/test-utils'import UserProfile from './UserProfile.vue'
// Modul mockenvi.mock('@/api', () => ({ fetchUser: vi.fn().mockResolvedValue({ name: 'Max', email: 'max@test.de' })}))
import { fetchUser } from '@/api'
describe('UserProfile', () => { it('displays user data', async () => { const wrapper = mount(UserProfile, { props: { userId: 1 } })
await flushPromises() // Async-Operationen abwarten
expect(fetchUser).toHaveBeenCalledWith(1) expect(wrapper.text()).toContain('Max') })})Mit Pinia testen
Abschnitt betitelt „Mit Pinia testen“import { describe, it, expect, beforeEach } from 'vitest'import { setActivePinia, createPinia } from 'pinia'import { mount } from '@vue/test-utils'import { useCounterStore } from '@/stores/counter'import CounterDisplay from './CounterDisplay.vue'
describe('CounterDisplay', () => { beforeEach(() => { setActivePinia(createPinia()) })
it('displays count from store', () => { const store = useCounterStore() store.count = 5
const wrapper = mount(CounterDisplay)
expect(wrapper.text()).toContain('5') })
it('calls store action', async () => { const wrapper = mount(CounterDisplay)
await wrapper.find('[data-test="increment"]').trigger('click')
expect(useCounterStore().count).toBe(1) })})Mit Vue Router testen
Abschnitt betitelt „Mit Vue Router testen“import { describe, it, expect } from 'vitest'import { mount } from '@vue/test-utils'import { createRouter, createMemoryHistory } from 'vue-router'import App from './App.vue'
describe('Router', () => { it('navigates to about page', async () => { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: { template: '<div>Home</div>' } }, { path: '/about', component: { template: '<div>About</div>' } } ] })
const wrapper = mount(App, { global: { plugins: [router] } })
await router.push('/about') await router.isReady()
expect(wrapper.text()).toContain('About') })})Async testen
Abschnitt betitelt „Async testen“import { describe, it, expect, vi } from 'vitest'import { mount, flushPromises } from '@vue/test-utils'
describe('Async', () => { // Mit async/await it('waits for async operation', async () => { const wrapper = mount(AsyncComponent)
await flushPromises() // Alle Promises abwarten
expect(wrapper.text()).toContain('Loaded') })
// Timer mocken it('handles setTimeout', async () => { vi.useFakeTimers() const wrapper = mount(DelayedComponent)
vi.advanceTimersByTime(1000) await flushPromises()
expect(wrapper.text()).toContain('Done') vi.useRealTimers() })})Test Coverage
Abschnitt betitelt „Test Coverage“# Coverage-Report generierennpm run coverageexport default defineConfig({ test: { coverage: { provider: 'v8', // oder 'istanbul' reporter: ['text', 'html', 'lcov'], exclude: ['node_modules', 'test/**'] } }})Best Practices
Abschnitt betitelt „Best Practices“data-testAttribute für Selektoren- Ein Konzept pro Test
- Aussagekräftige Test-Namen
- AAA-Pattern (Arrange, Act, Assert)
- Async mit
await/flushPromises - Tests isoliert halten
- Implementation testen (nur Verhalten)
- Auf Klassen/IDs verlassen
- Zu viel in einem Test
- Snapshots übermäßig nutzen
- Tests voneinander abhängig machen
- Interna von Komponenten testen
Dokumentation
Abschnitt betitelt „Dokumentation“Offizielle Dokumentation:
- Vitest - Test Framework
- Vue Test Utils - Komponenten-Testing
- Testing Vue - Vue Guide
API Referenz:
- Vitest API - describe, it, expect, vi
- VTU API - mount, wrapper methods