1 min read
Web ScrapingVitest: Next-Gen Testing mit Browser Mode
Vitest als Vite-native Testing Framework. Browser Mode, Component Testing, TypeScript-Support und Migration von Jest.
VitestTestingBrowser ModeComponent TestingPlaywrightReact Testing

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
# Installation
npm install -D vitest
# Browser Mode (optional)
npm install -D @vitest/browser playwright
# UI (optional)
npm install -D @vitest/ui// 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']
}
}
});// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});Basic Unit Testing
// 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<unknown> {
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)
// 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 (
<div>
<span data-testid="count">{count}</span>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
// 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(<Counter initialValue={5} />);
expect(screen.getByTestId('count')).toHaveTextContent('5');
});
it('increments count when + clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByText('+'));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
it('decrements count when - clicked', () => {
render(<Counter initialValue={5} />);
fireEvent.click(screen.getByText('-'));
expect(screen.getByTestId('count')).toHaveTextContent('4');
});
it('calls onCountChange callback', () => {
const handleChange = vi.fn();
render(<Counter onCountChange={handleChange} />);
fireEvent.click(screen.getByText('+'));
expect(handleChange).toHaveBeenCalledWith(1);
});
});Browser Mode (Real Browser Testing)
# Browser Mode initialisieren
npx vitest init browser// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright', // oder 'webdriverio'
name: 'chromium',
headless: true
}
}
});// 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(
<Dialog trigger={<button>Open</button>}>
<p>Dialog Content</p>
</Dialog>
);
// 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(
<Dialog trigger={<button>Open</button>}>
<input data-testid="input1" />
<input data-testid="input2" />
<button>Close</button>
</Dialog>
);
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
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<typeof import('./utils')>();
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
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(
<UserCard
user={{
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatar.jpg'
}}
/>
);
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
// 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
}
}
}
});# Coverage generieren
npx vitest --coverage
# Watch Mode mit Coverage
npx vitest --coverage --watchParallel & Sequential Tests
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
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
// jest.config.js → vitest.config.ts
// Jest
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/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:
- Vite-Native: Shared Config, HMR, sofortiger Start
- Browser Mode: Echte Browser statt JSDOM
- TypeScript-First: Out-of-Box Support ohne Config
- Jest-kompatibel: Einfache Migration
Für jedes Vite-Projekt ist Vitest die natürliche Wahl.
Bildprompts
- "Test runner showing green checkmarks, development workflow visualization"
- "Browser and code split screen, component testing in real browser concept"
- "Fast forward icon with test results, instant feedback development loop"