Zum Inhalt springen

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
Terminal-Fenster
npm install -D vitest @vue/test-utils happy-dom
vite.config.js
import { 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
}
})
package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
src/utils/math.test.js
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)
})
})
})
// Gleichheit
expect(value).toBe(5) // ===
expect(obj).toEqual({ a: 1 }) // Deep equal
expect(value).toStrictEqual(x) // Strict deep
// Wahrheitswerte
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeDefined()
// Zahlen
expect(num).toBeGreaterThan(3)
expect(num).toBeLessThanOrEqual(5)
expect(0.1 + 0.2).toBeCloseTo(0.3)
src/components/Counter.test.js
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')
})
})
import { mount } from '@vue/test-utils'
const wrapper = mount(ParentComponent)
  • Rendert alle Kind-Komponenten
  • Für Integrationstests
  • Langsamer
const wrapper = mount(MyComponent)
// Per CSS-Selektor
wrapper.find('button') // Erstes Element
wrapper.findAll('li') // Alle Elemente
wrapper.find('.submit-btn') // Per Klasse
wrapper.find('#user-input') // Per ID
wrapper.find('[data-test="submit"]') // Per data-Attribut (Empfohlen!)
// Per Komponente
import ChildComponent from './ChildComponent.vue'
wrapper.findComponent(ChildComponent)
wrapper.findAllComponents(ChildComponent)
// Prüfungen
wrapper.find('button').exists() // true/false
wrapper.find('.error').isVisible() // Sichtbarkeit
// 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!')
})
})
// 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])
})
})
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'
})
})
})
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()
})
})
// 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 mocken
vi.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')
})
})
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)
})
})
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')
})
})
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()
})
})
Terminal-Fenster
# Coverage-Report generieren
npm run coverage
vite.config.js
export default defineConfig({
test: {
coverage: {
provider: 'v8', // oder 'istanbul'
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules', 'test/**']
}
}
})
  • data-test Attribute 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

Offizielle Dokumentation:

API Referenz: