# Vitest: Next-Gen Testing mit Browser Mode **Meta-Description:** Vitest als Vite-native Testing Framework. Browser Mode, Component Testing, TypeScript-Support und Migration von Jest. **Keywords:** Vitest, Testing, Browser Mode, Component Testing, Playwright, React Testing, TypeScript, Jest Alternative --- ## Einführung Vitest ist das **native Testing-Framework für Vite** und hat Jest in modernen Projekten weitgehend abgelöst. Mit **Browser Mode** testet Vitest Components in echten Browsern – ohne JSDOM-Limitierungen. --- ## Warum Vitest? ``` ┌─────────────────────────────────────────────────────────────┐ │ VITEST vs JEST │ ├─────────────────────────────────────────────────────────────┤ │ │ │ VITEST JEST │ │ ──────────────────── ──────────────────── │ │ Vite-Native Babel-basiert │ │ ESM First CommonJS Default │ │ Shared Config mit Vite Separate Config │ │ Hot Module Replacement Full Restart │ │ Browser Mode JSDOM only │ │ │ │ Performance: │ │ ├── Instant Watch Mode │ │ ├── Native TypeScript Support │ │ ├── Out-of-Box ESM Support │ │ └── Shared Vite Pipeline │ │ │ │ Bundle: ~5MB vs ~65MB (Jest + Babel) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Quick Setup ```bash # Installation npm install -D vitest # Browser Mode (optional) npm install -D @vitest/browser playwright # UI (optional) npm install -D @vitest/ui ``` ```typescript // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'], coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'] } } }); ``` ```typescript // src/test/setup.ts import '@testing-library/jest-dom'; import { cleanup } from '@testing-library/react'; import { afterEach } from 'vitest'; afterEach(() => { cleanup(); }); ``` --- ## Basic Unit Testing ```typescript // src/utils/math.ts export function add(a: number, b: number): number { return a + b; } export function multiply(a: number, b: number): number { return a * b; } export async function fetchData(url: string): Promise { const response = await fetch(url); return response.json(); } // src/utils/math.test.ts import { describe, it, expect, vi } from 'vitest'; import { add, multiply, fetchData } from './math'; describe('Math Utils', () => { it('should add two numbers', () => { expect(add(2, 3)).toBe(5); expect(add(-1, 1)).toBe(0); }); it('should multiply two numbers', () => { expect(multiply(2, 3)).toBe(6); expect(multiply(0, 100)).toBe(0); }); }); describe('fetchData', () => { it('should fetch and parse JSON', async () => { const mockData = { name: 'Test' }; // Mock fetch vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve(mockData) })); const result = await fetchData('/api/test'); expect(result).toEqual(mockData); vi.unstubAllGlobals(); }); }); ``` --- ## Component Testing (JSDOM) ```typescript // src/components/Counter.tsx import { useState } from 'react'; interface CounterProps { initialValue?: number; onCountChange?: (count: number) => void; } export function Counter({ initialValue = 0, onCountChange }: CounterProps) { const [count, setCount] = useState(initialValue); const increment = () => { const newCount = count + 1; setCount(newCount); onCountChange?.(newCount); }; const decrement = () => { const newCount = count - 1; setCount(newCount); onCountChange?.(newCount); }; return (
{count}
); } // src/components/Counter.test.tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { Counter } from './Counter'; describe('Counter', () => { it('renders with initial value', () => { render(); expect(screen.getByTestId('count')).toHaveTextContent('5'); }); it('increments count when + clicked', () => { render(); fireEvent.click(screen.getByText('+')); expect(screen.getByTestId('count')).toHaveTextContent('1'); }); it('decrements count when - clicked', () => { render(); fireEvent.click(screen.getByText('-')); expect(screen.getByTestId('count')).toHaveTextContent('4'); }); it('calls onCountChange callback', () => { const handleChange = vi.fn(); render(); fireEvent.click(screen.getByText('+')); expect(handleChange).toHaveBeenCalledWith(1); }); }); ``` --- ## Browser Mode (Real Browser Testing) ```bash # Browser Mode initialisieren npx vitest init browser ``` ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { browser: { enabled: true, provider: 'playwright', // oder 'webdriverio' name: 'chromium', headless: true } } }); ``` ```typescript // src/components/Dialog.browser.test.tsx import { describe, it, expect } from 'vitest'; import { render } from 'vitest-browser-react'; import { Dialog } from './Dialog'; describe('Dialog (Browser Mode)', () => { it('should open and close correctly', async () => { const screen = render( Open}>

Dialog Content

); // Initial: Dialog geschlossen await expect.element(screen.getByText('Dialog Content')).not.toBeVisible(); // Öffnen await screen.getByText('Open').click(); await expect.element(screen.getByText('Dialog Content')).toBeVisible(); // Schließen mit Escape await screen.getByRole('dialog').press('Escape'); await expect.element(screen.getByText('Dialog Content')).not.toBeVisible(); }); it('should focus trap correctly', async () => { const screen = render( Open}> ); await screen.getByText('Open').click(); // Fokus sollte im Dialog gefangen sein const input1 = screen.getByTestId('input1'); const input2 = screen.getByTestId('input2'); await input1.focus(); await input1.press('Tab'); await expect.element(input2).toBeFocused(); }); }); ``` --- ## Mocking ```typescript import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // 1. Function Mocking const mockFn = vi.fn(); mockFn.mockReturnValue(42); mockFn.mockResolvedValue({ data: 'test' }); mockFn.mockImplementation((x) => x * 2); // 2. Module Mocking vi.mock('./api', () => ({ fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }), updateUser: vi.fn().mockResolvedValue({ success: true }) })); // 3. Partial Mocking vi.mock('./utils', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, complexFunction: vi.fn().mockReturnValue('mocked') }; }); // 4. Timer Mocking describe('Timer Tests', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should handle setTimeout', async () => { const callback = vi.fn(); setTimeout(callback, 1000); expect(callback).not.toHaveBeenCalled(); vi.advanceTimersByTime(1000); expect(callback).toHaveBeenCalledOnce(); }); it('should handle intervals', () => { const callback = vi.fn(); setInterval(callback, 100); vi.advanceTimersByTime(350); expect(callback).toHaveBeenCalledTimes(3); }); }); // 5. Date Mocking it('should mock current date', () => { const mockDate = new Date('2026-01-15'); vi.setSystemTime(mockDate); expect(new Date().toISOString()).toContain('2026-01-15'); vi.useRealTimers(); }); ``` --- ## Snapshot Testing ```typescript import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import { UserCard } from './UserCard'; describe('UserCard Snapshots', () => { it('matches snapshot for basic user', () => { const { container } = render( ); expect(container).toMatchSnapshot(); }); it('matches inline snapshot', () => { const user = { name: 'Jane', role: 'admin' }; expect(user).toMatchInlineSnapshot(` { "name": "Jane", "role": "admin", } `); }); // File Snapshots it('matches file snapshot for large data', () => { const complexData = generateLargeDataset(); expect(complexData).toMatchFileSnapshot('./snapshots/large-data.json'); }); }); ``` --- ## Test Coverage ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { coverage: { provider: 'v8', // oder 'istanbul' reporter: ['text', 'html', 'lcov', 'json'], reportsDirectory: './coverage', include: ['src/**/*.{ts,tsx}'], exclude: [ 'src/**/*.test.{ts,tsx}', 'src/**/*.d.ts', 'src/test/**/*' ], thresholds: { statements: 80, branches: 80, functions: 80, lines: 80 } } } }); ``` ```bash # Coverage generieren npx vitest --coverage # Watch Mode mit Coverage npx vitest --coverage --watch ``` --- ## Parallel & Sequential Tests ```typescript import { describe, it, expect } from 'vitest'; // Parallel (Default) describe('Parallel Tests', () => { it.concurrent('test 1', async () => { await sleep(1000); expect(true).toBe(true); }); it.concurrent('test 2', async () => { await sleep(1000); expect(true).toBe(true); }); // Beide laufen parallel → ~1s total }); // Sequential describe('Sequential Tests', { sequential: true }, () => { let sharedState = 0; it('first', () => { sharedState = 1; expect(sharedState).toBe(1); }); it('second', () => { sharedState = 2; expect(sharedState).toBe(2); }); }); // Test Isolation describe.concurrent('Isolated Concurrent', () => { // Jeder Test bekommt eigene Isolation it('isolated 1', async ({ expect }) => { // ... }); it('isolated 2', async ({ expect }) => { // ... }); }); ``` --- ## API Testing ```typescript import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; const server = setupServer( http.get('/api/users', () => { return HttpResponse.json([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]); }), http.post('/api/users', async ({ request }) => { const body = await request.json(); return HttpResponse.json({ id: 3, ...body }, { status: 201 }); }) ); beforeAll(() => server.listen()); afterAll(() => server.close()); describe('API Client', () => { it('fetches users', async () => { const response = await fetch('/api/users'); const users = await response.json(); expect(users).toHaveLength(2); expect(users[0].name).toBe('Alice'); }); it('creates a user', async () => { const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify({ name: 'Charlie' }) }); expect(response.status).toBe(201); const user = await response.json(); expect(user.name).toBe('Charlie'); }); }); ``` --- ## Migration von Jest ```typescript // jest.config.js → vitest.config.ts // Jest module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['/src/setupTests.ts'], moduleNameMapper: { '^@/(.*)$': '/src/$1' } }; // Vitest import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', setupFiles: './src/setupTests.ts', alias: { '@': './src' } } }); // API-Änderungen // Jest: jest.fn() → Vitest: vi.fn() // Jest: jest.mock() → Vitest: vi.mock() // Jest: jest.spyOn() → Vitest: vi.spyOn() ``` --- ## Fazit Vitest bietet: 1. **Vite-Native**: Shared Config, HMR, sofortiger Start 2. **Browser Mode**: Echte Browser statt JSDOM 3. **TypeScript-First**: Out-of-Box Support ohne Config 4. **Jest-kompatibel**: Einfache Migration Für jedes Vite-Projekt ist Vitest die natürliche Wahl. --- ## Bildprompts 1. "Test runner showing green checkmarks, development workflow visualization" 2. "Browser and code split screen, component testing in real browser concept" 3. "Fast forward icon with test results, instant feedback development loop" --- ## Quellen - [Vitest Documentation](https://vitest.dev/) - [Vitest Browser Mode](https://vitest.dev/guide/browser/) - [vitest-browser-react](https://github.com/vitest-dev/vitest-browser-react) - [Vitest vs JSDOM - InfoQ](https://www.infoq.com/news/2025/06/vitest-browser-mode-jsdom/)