Testen von Anwendungen mit Vitest und React Testing Library
Testen von Anwendungen
poradnikiTesten von Anwendungen mit Vitest und React Testing Library
Testen ist ein wesentlicher Bestandteil der professionellen Softwareentwicklung. Gut geschriebene Tests geben Ihnen die Sicherheit, dass Ihre Anwendung wie erwartet funktioniert, schützen vor Regressionen und erleichtern das Refactoring. Im React-Ökosystem hat Jest jahrelang dominiert, aber Vitest wird dank seiner blitzschnellen Geschwindigkeit, nativer ESM-Unterstützung und hervorragender Integration mit Vite schnell zum neuen Standard. In diesem Leitfaden zeigen wir Ihnen, wie Sie Vitest konfigurieren, Tests für React-Komponenten mit Testing Library schreiben, Abhängigkeiten mocken, Hooks und APIs testen und vieles mehr.
Warum Vitest statt Jest?#
Jest ist ein bewährtes Test-Framework, das der React-Community jahrelang gut gedient hat. Vitest bietet jedoch einige bedeutende Vorteile, die es für Ihr nächstes Projekt oder eine Migration in Betracht zu ziehen lohnt.
Geschwindigkeit#
Vitest nutzt die Transformationen von Vite (esbuild/SWC), was eine deutlich schnellere TypeScript- und JSX-Kompilierung bedeutet als das von Jest verwendete Babel. Tests starten nahezu sofort, und der Watch-Modus reagiert in Millisekunden auf Änderungen.
Native ESM-Unterstützung#
Vitest unterstützt nativ ES-Module, was die Probleme bei der Konfiguration von transformIgnorePatterns und moduleNameMapper beseitigt, die mit Jest häufig Schwierigkeiten verursachen.
API-Kompatibilität#
Die API von Vitest ist weitgehend kompatibel mit Jest. Funktionen wie describe, it, expect, vi.fn() und vi.mock() funktionieren analog, was die Migration bestehender Tests unkompliziert macht.
Vite-Integration#
Wenn Ihr Projekt Vite als Bundler verwendet, teilt Vitest dieselbe Konfiguration. Sie müssen keine separate Konfiguration für Tests pflegen — Aliase, Plugins und Transformationen werden geteilt.
// vite.config.ts - shared configuration
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@utils': '/src/utils',
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
css: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
Projektkonfiguration#
Beginnen wir mit der Installation der erforderlichen Abh\u00e4ngigkeiten.
Installation#
# Vitest and DOM environment
npm install -D vitest @vitest/coverage-v8 jsdom
# React Testing Library
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
# MSW for API mocking
npm install -D msw
Setup-Datei#
Erstellen Sie eine Setup-Datei, die vor jeder Testdatei geladen wird.
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
// Automatic cleanup after each test
afterEach(() => {
cleanup();
});
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
value: MockIntersectionObserver,
});
Package.json Scripts#
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Testen von React-Komponenten#
React Testing Library fördert das Testen von Komponenten aus der Perspektive des Benutzers. Anstatt Implementierungsdetails zu testen, konzentrieren wir uns darauf, was der Benutzer sieht und womit er interagiert.
Grundlegender Komponententest#
// src/components/Button/Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
}
export function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
data-testid="custom-button"
>
{label}
</button>
);
}
// src/components/Button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders the button with the provided text', () => {
render(<Button label="Click me" onClick={() => {}} />);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button label="Save" onClick={handleClick} />);
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when the button is disabled', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button label="Save" onClick={handleClick} disabled />);
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(handleClick).not.toHaveBeenCalled();
});
it('applies the correct CSS class for the variant', () => {
render(<Button label="Delete" onClick={() => {}} variant="danger" />);
expect(screen.getByRole('button')).toHaveClass('btn-danger');
});
});
Testing Forms#
Formulare geh\u00f6ren zu den am h\u00e4ufigsten getesteten Elementen in Anwendungen. Testing Library bietet Werkzeuge zur Simulation von Benutzerinteraktionen mit Formularfeldern.
// src/components/LoginForm/LoginForm.tsx
import { useState } from 'react';
interface LoginFormProps {
onSubmit: (credentials: { email: string; password: string }) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email || !password) {
setError('Please fill in all fields');
return;
}
setIsLoading(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError('Invalid login credentials');
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="Login form">
{error && <div role="alert">{error}</div>}
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign in'}
</button>
</form>
);
}
// src/components/LoginForm/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
const mockSubmit = vi.fn();
beforeEach(() => {
mockSubmit.mockReset();
});
it('shows an error when fields are empty', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.click(screen.getByRole('button', { name: 'Sign in' }));
expect(screen.getByRole('alert')).toHaveTextContent('Please fill in all fields');
expect(mockSubmit).not.toHaveBeenCalled();
});
it('submits the form with valid data', async () => {
const user = userEvent.setup();
mockSubmit.mockResolvedValueOnce(undefined);
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.type(screen.getByLabelText('Password'), 'secretpassword123');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'john@example.com',
password: 'secretpassword123',
});
});
});
it('shows loading state during submission', async () => {
const user = userEvent.setup();
mockSubmit.mockImplementation(() => new Promise(() => {}));
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
expect(screen.getByRole('button', { name: 'Signing in...' })).toBeDisabled();
});
it('displays an error on failed login', async () => {
const user = userEvent.setup();
mockSubmit.mockRejectedValueOnce(new Error('Unauthorized'));
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.type(screen.getByLabelText('Password'), 'wrongpassword');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Invalid login credentials');
});
});
});
Mocking mit vi.mock und vi.fn#
Vitest bietet leistungsstarke Werkzeuge zum Mocking von Modulen und Funktionen. Damit k\u00f6nnen Sie den zu testenden Code von externen Abh\u00e4ngigkeiten isolieren.
Modul-Mocking#
// src/services/api.ts
export async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
}
export async function createUser(data: CreateUserDto): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
}
// src/components/UserList/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserList } from './UserList';
import * as api from '@/services/api';
// Mock the entire module
vi.mock('@/services/api', () => ({
fetchUsers: vi.fn(),
}));
const mockFetchUsers = vi.mocked(api.fetchUsers);
describe('UserList', () => {
it('displays a list of users', async () => {
mockFetchUsers.mockResolvedValueOnce([
{ id: 1, name: 'John Smith', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
]);
render(<UserList />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Smith')).toBeInTheDocument();
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
});
it('displays an error when fetching fails', async () => {
mockFetchUsers.mockRejectedValueOnce(new Error('Network error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load users');
});
});
});
Funktions-Mocking und Spying#
// Creating a mock with implementation
const mockCallback = vi.fn((x: number) => x * 2);
// Checking invocations
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(5);
expect(mockCallback).toHaveReturnedWith(10);
// Spying on existing methods
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
// ... test
expect(spy).toHaveBeenCalledWith('Something went wrong');
spy.mockRestore();
// Mocking timers
vi.useFakeTimers();
setTimeout(() => callback(), 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
Snapshot Testing#
Snapshot-Tests sind nützlich, um unerwartete Änderungen im gerenderten HTML zu erkennen. Vitest unterstützt sowohl traditionelle dateibasierte Snapshots als auch Inline-Snapshots.
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Card } from './Card';
describe('Card', () => {
it('renders the correct HTML structure', () => {
const { container } = render(
<Card title="Example title" description="Card description">
<p>Card content</p>
</Card>
);
expect(container.firstChild).toMatchSnapshot();
});
it('renders with inline snapshot', () => {
const { container } = render(<Card title="Test" />);
expect(container.firstChild).toMatchInlineSnapshot(`
<div class="card">
<h3 class="card-title">Test</h3>
</div>
`);
});
});
Denken Sie daran, dass Snapshots sparsam eingesetzt werden sollten. \u00dcberm\u00e4\u00dfig gro\u00dfe Snapshots werden schwer \u00fcberpr\u00fcfbar und werden oft gedankenlos aktualisiert, was ihren diagnostischen Wert verringert.
Code-Coverage#
Vitest verwendet den Provider v8 oder istanbul zur Messung der Code-Abdeckung.
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/types/**',
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
});
Tests mit Coverage ausführen:
npx vitest run --coverage
Der HTML-Bericht wird im Verzeichnis coverage/ generiert und kann im Browser geöffnet werden.
Asynchrones Testen#
React-Anwendungen f\u00fchren h\u00e4ufig asynchrone Operationen durch \u2014 Datenabruf, Timer, Animationen. Vitest und Testing Library bieten Werkzeuge zu deren Testen.
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// src/components/SearchBox/SearchBox.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SearchBox } from './SearchBox';
describe('SearchBox with debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('calls onSearch after the debounce delay', async () => {
const onSearch = vi.fn();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<SearchBox onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole('searchbox'), 'React');
// Before debounce expires - should not be called
expect(onSearch).not.toHaveBeenCalled();
// After debounce expires
vi.advanceTimersByTime(300);
await waitFor(() => {
expect(onSearch).toHaveBeenCalledWith('React');
});
});
});
API-Mocking mit MSW#
Mock Service Worker (MSW) ist ein hervorragendes Werkzeug zum Mocking von APIs auf Netzwerkebene. Anstatt fetch oder axios zu mocken, f\u00e4ngt MSW HTTP-Anfragen ab und gibt definierte Antworten zur\u00fcck.
MSW Configuration#
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John Smith', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' },
];
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json(users);
}),
http.get('/api/users/:id', ({ params }) => {
const user = users.find((u) => u.id === Number(params.id));
if (!user) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(user);
}),
http.post('/api/users', async ({ request }) => {
const body = (await request.json()) as Omit<User, 'id'>;
const newUser: User = { id: Date.now(), ...body };
return HttpResponse.json(newUser, { status: 201 });
}),
http.delete('/api/users/:id', () => {
return new HttpResponse(null, { status: 204 });
}),
];
// src/test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Integration with Vitest#
// src/test/setup.ts - add MSW configuration
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Verwendung von MSW in Tests#
// src/components/UserProfile/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { http, HttpResponse } from 'msw';
import { server } from '@/test/mocks/server';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('displays user data', async () => {
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John Smith')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});
it('shows a message when the user is not found', async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 404 });
})
);
render(<UserProfile userId={999} />);
await waitFor(() => {
expect(screen.getByText('User not found')).toBeInTheDocument();
});
});
it('handles server errors', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.error();
})
);
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Error loading data');
});
});
});
Testing Hooks#
Custom Hooks können mit renderHook aus React Testing Library getestet werden.
// src/hooks/useLocalStorage.ts
import { useState, useCallback } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
},
[key, storedValue]
);
const removeValue = useCallback(() => {
setStoredValue(initialValue);
window.localStorage.removeItem(key);
}, [key, initialValue]);
return [storedValue, setValue, removeValue] as const;
}
// src/hooks/useLocalStorage.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import { useLocalStorage } from './useLocalStorage';
describe('useLocalStorage', () => {
beforeEach(() => {
localStorage.clear();
});
it('returns the initial value when localStorage is empty', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('default');
});
it('reads an existing value from localStorage', () => {
localStorage.setItem('key', JSON.stringify('saved'));
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('saved');
});
it('updates the value in state and localStorage', () => {
const { result } = renderHook(() => useLocalStorage('key', 'initial'));
act(() => {
result.current[1]('updated');
});
expect(result.current[0]).toBe('updated');
expect(JSON.parse(localStorage.getItem('key')!)).toBe('updated');
});
it('supports functional updates', () => {
const { result } = renderHook(() => useLocalStorage('count', 0));
act(() => {
result.current[1]((prev) => prev + 1);
});
expect(result.current[0]).toBe(1);
});
it('removes the value from localStorage', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
act(() => {
result.current[1]('value');
});
act(() => {
result.current[2]();
});
expect(result.current[0]).toBe('default');
expect(localStorage.getItem('key')).toBeNull();
});
});
Integrationstests#
Integrationstests \u00fcberpr\u00fcfen das Zusammenspiel mehrerer Komponenten. Sie testen realistische Nutzungsszenarien, ohne interne Abh\u00e4ngigkeiten zu mocken.
// src/features/TodoApp/TodoApp.test.tsx
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { TodoApp } from './TodoApp';
describe('TodoApp - integration test', () => {
it('allows adding, completing, and deleting tasks', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// Add a new task
const input = screen.getByPlaceholderText('Add a new task...');
await user.type(input, 'Write tests{Enter}');
expect(screen.getByText('Write tests')).toBeInTheDocument();
expect(input).toHaveValue('');
// Add a second task
await user.type(input, 'Review PR{Enter}');
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(2);
// Mark task as completed
const firstItem = items[0];
const checkbox = within(firstItem).getByRole('checkbox');
await user.click(checkbox);
expect(checkbox).toBeChecked();
expect(firstItem).toHaveClass('completed');
// Delete the task
const deleteButton = within(firstItem).getByRole('button', { name: /delete/i });
await user.click(deleteButton);
expect(screen.queryByText('Write tests')).not.toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(1);
});
it('filters tasks by status', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// Add tasks
const input = screen.getByPlaceholderText('Add a new task...');
await user.type(input, 'Task 1{Enter}');
await user.type(input, 'Task 2{Enter}');
// Mark the first as completed
const items = screen.getAllByRole('listitem');
await user.click(within(items[0]).getByRole('checkbox'));
// Filter active
await user.click(screen.getByRole('button', { name: 'Active' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('Task 2')).toBeInTheDocument();
// Filter completed
await user.click(screen.getByRole('button', { name: 'Completed' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('Task 1')).toBeInTheDocument();
});
});
CI/CD-Integration#
Vitest integriert sich nahtlos mit popul\u00e4ren CI/CD-Plattformen. Hier ist eine Beispielkonfiguration f\u00fcr GitHub Actions.
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npx vitest run --coverage --reporter=junit --outputFile=test-results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.node-version }}
path: test-results.xml
Configuring Reporters#
Vitest unterstützt verschiedene Berichtsformate, die in CI-Umgebungen nützlich sind.
// vitest.config.ts
export default defineConfig({
test: {
reporters: ['default', 'junit', 'json'],
outputFile: {
junit: './test-results/junit.xml',
json: './test-results/results.json',
},
},
});
Best Practices#
Die Befolgung bew\u00e4hrter Testpraktiken hilft Ihnen, Tests zu schreiben, die leicht zu warten, lesbar sind und echten Wert liefern.
1. Verhalten testen, nicht Implementierung#
// Bad - testing implementation details
expect(component.state.isOpen).toBe(true);
// Good - testing from the user's perspective
expect(screen.getByRole('dialog')).toBeVisible();
2. Abfragen nach Priorität verwenden#
React Testing Library empfiehlt die folgende Abfragepriorit\u00e4t:
getByRole- am zug\u00e4nglichstengetByLabelText- gut f\u00fcr FormularfeldergetByPlaceholderText- wenn kein Label vorhandengetByText- f\u00fcr TextinhaltegetByTestId- letzter Ausweg
3. Implementierungsdetails nicht testen#
// Bad - relying on internal state
const { result } = renderHook(() => useCounter());
expect(result.current.state.internalFlag).toBe(true);
// Good - testing the public API
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
4. Tests nach AAA organisieren (Arrange, Act, Assert)#
it('adds a product to the cart', async () => {
// Arrange
const user = userEvent.setup();
render(<ProductPage productId="123" />);
await waitFor(() => expect(screen.getByText('Test Product')).toBeInTheDocument());
// Act
await user.click(screen.getByRole('button', { name: 'Add to cart' }));
// Assert
expect(screen.getByText('Added to cart')).toBeInTheDocument();
});
5. Render-Helfer erstellen#
// src/test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/contexts/ThemeContext';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
}
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
queryClient?: QueryClient;
}
export function renderWithProviders(
ui: ReactElement,
{ queryClient = createTestQueryClient(), ...options }: CustomRenderOptions = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ThemeProvider>{children}</ThemeProvider>
</BrowserRouter>
</QueryClientProvider>
);
}
return {
...render(ui, { wrapper: Wrapper, ...options }),
queryClient,
};
}
export { screen, waitFor, within } from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
6. Testdaten-Factories verwenden#
// src/test/factories.ts
interface UserFactory {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
let nextId = 1;
export function createUser(overrides: Partial<UserFactory> = {}): UserFactory {
return {
id: nextId++,
name: `User ${nextId}`,
email: `user${nextId}@example.com`,
role: 'user',
...overrides,
};
}
export function createUsers(count: number, overrides: Partial<UserFactory> = {}): UserFactory[] {
return Array.from({ length: count }, () => createUser(overrides));
}
Zusammenfassung#
Vitest in Kombination mit React Testing Library bildet ein leistungsstarkes Toolkit zum Testen von React-Anwendungen. Die wichtigsten Erkenntnisse aus diesem Leitfaden:
- Vitest bietet blitzschnelle Geschwindigkeit, native ESM-Unterstützung und hervorragende Vite-Integration
- React Testing Library f\u00f6rdert das Testen aus der Perspektive des Benutzers, was zu wertvolleren Tests f\u00fchrt
- MSW ermöglicht realistisches API-Mocking auf Netzwerkebene
- Hook-Tests k\u00f6nnen mit
renderHookgeschrieben werden, ohne Wrapper-Komponenten erstellen zu m\u00fcssen - Integrationstests überprüfen realistische Nutzungsszenarien
- CI/CD-Integration stellt sicher, dass Tests bei jeder \u00c4nderung automatisch ausgef\u00fchrt werden
Denken Sie daran, dass Tests Ihnen Vertrauen geben sollen, dass Ihre Anwendung korrekt funktioniert, und nicht zur Last werden sollten. Konzentrieren Sie sich auf das Testen wichtiger Gesch\u00e4ftsszenarien und kritischer Benutzerpfade.
Ben\u00f6tigen Sie Hilfe beim Aufbau einer gut getesteten React-Anwendung? Das Team von MDS Software Solutions Group ist spezialisiert auf die Erstellung zuverl\u00e4ssiger Webanwendungen mit umfassender Testabdeckung. Kontaktieren Sie uns, um Ihr Projekt zu besprechen.
Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.