Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
# 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<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)
|
||||
|
||||
```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 (
|
||||
<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)
|
||||
|
||||
```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(
|
||||
<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
|
||||
|
||||
```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<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
|
||||
|
||||
```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(
|
||||
<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
|
||||
|
||||
```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: ['<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:
|
||||
|
||||
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/)
|
||||
Reference in New Issue
Block a user