Unit-Tests für smolitux UI Komponenten
Dieses Dokument beschreibt die Strategie und Implementierung von Unit-Tests für die Komponenten der smolitux UI-Bibliothek.
1. Allgemeine Teststrategie
Für jede Komponente sollten folgende Aspekte getestet werden:
- Rendering: Die Komponente wird korrekt gerendert
- Props: Verhaltensänderungen basierend auf verschiedenen Props
- Interaktionen: Benutzerinteraktionen wie Klicks, Hover, etc.
- Zustände: Verschiedene Zustände wie Loading, Error, Disabled
- Accessibility: Tests für grundlegende A11y-Eigenschaften
2. Best Practices für Unit-Tests
- Isolation: Jeder Test sollte isoliert von anderen Tests ausgeführt werden können
- Unabhängigkeit: Tests sollten nicht von der Reihenfolge der Ausführung abhängen
- Lesbarkeit: Tests sollten klar und verständlich sein
- Wartbarkeit: Tests sollten einfach zu warten sein
3. Beispiele für Unit-Tests
3.1 Beispiel: Button-Komponente
// Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '../Button';
describe('Button', () => {
test('renders correctly with default props', () => {
render(<Button>Click me</Button>);
const button = screen.getByRole('button', { name: /Click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('bg-primary-600'); // Primär-Variante als Default
});
test('applies different variant styles correctly', () => {
const { rerender } = render(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-secondary-600');
rerender(<Button variant="outline">Outline</Button>);
expect(screen.getByRole('button')).toHaveClass('border-gray-300');
rerender(<Button variant="ghost">Ghost</Button>);
expect(screen.getByRole('button')).toHaveClass('text-gray-700');
});
test('renders with different sizes', () => {
const { rerender } = render(<Button size="sm">Small</Button>);
expect(screen.getByRole('button')).toHaveClass('px-3 py-1.5 text-sm');
rerender(<Button size="lg">Large</Button>);
expect(screen.getByRole('button')).toHaveClass('px-6 py-3 text-lg');
});
test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('renders loading state correctly', () => {
render(<Button loading>Loading</Button>);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeDisabled();
});
test('renders icons correctly', () => {
render(
<Button leftIcon={<span data-testid="left-icon" />}>
With Icon
</Button>
);
expect(screen.getByTestId('left-icon')).toBeInTheDocument();
});
test('applies fullWidth class when fullWidth prop is true', () => {
render(<Button fullWidth>Full Width</Button>);
expect(screen.getByRole('button')).toHaveClass('w-full');
});
test('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('button')).toHaveClass('opacity-50');
});
test('passes additional HTML attributes to button element', () => {
render(<Button data-testid="custom-button">Custom Attr</Button>);
expect(screen.getByTestId('custom-button')).toBeInTheDocument();
});
});
3.2 Beispiel: Input-Komponente
// Input.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Input } from '../Input';
describe('Input', () => {
test('renders with default props', () => {
render(<Input />);
expect(screen.getByRole('textbox')).toBeInTheDocument();
});
test('renders with correct value and handles change', async () => {
const handleChange = jest.fn();
render(<Input value="" onChange={handleChange} />);
const input = screen.getByRole('textbox');
await userEvent.type(input, 'Hello');
expect(handleChange).toHaveBeenCalledTimes(5); // Einmal pro Zeichen
});
test('shows error message when provided', () => {
render(<Input error="Required field" />);
expect(screen.getByText('Required field')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
});
test('shows helper text when provided', () => {
render(<Input helperText="Enter your name" />);
expect(screen.getByText('Enter your name')).toBeInTheDocument();
});
test('applies disabled styles when disabled', () => {
render(<Input disabled />);
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.getByRole('textbox')).toHaveClass('bg-gray-100');
});
test('displays left and right icons correctly', () => {
const leftIcon = <span data-testid="left-icon">L</span>;
const rightIcon = <span data-testid="right-icon">R</span>;
render(<Input leftIcon={leftIcon} rightIcon={rightIcon} />);
expect(screen.getByTestId('left-icon')).toBeInTheDocument();
expect(screen.getByTestId('right-icon')).toBeInTheDocument();
});
test('handles focus and blur events', () => {
const handleFocus = jest.fn();
const handleBlur = jest.fn();
render(<Input onFocus={handleFocus} onBlur={handleBlur} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalledTimes(1);
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
test('applies custom className correctly', () => {
render(<Input className="custom-class" />);
expect(screen.getByRole('textbox')).toHaveClass('custom-class');
});
});
3.3 Beispiel: Card-Komponente
// Card.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Card } from '../Card';
describe('Card', () => {
test('renders children correctly', () => {
render(
<Card>
<div data-testid="card-content">Card Content</div>
</Card>
);
expect(screen.getByTestId('card-content')).toBeInTheDocument();
});
test('renders title when provided', () => {
render(<Card title="Card Title">Content</Card>);
expect(screen.getByText('Card Title')).toBeInTheDocument();
});
test('renders footer when provided', () => {
const footer = <div data-testid="card-footer">Footer</div>;
render(<Card footer={footer}>Content</Card>);
expect(screen.getByTestId('card-footer')).toBeInTheDocument();
});
test('applies different elevations correctly', () => {
const { rerender } = render(<Card elevation="low">Content</Card>);
expect(screen.getByText('Content').closest('.card')).toHaveClass('shadow-sm');
rerender(<Card elevation="high">Content</Card>);
expect(screen.getByText('Content').closest('.card')).toHaveClass('shadow-lg');
});
test('applies custom className correctly', () => {
render(<Card className="custom-card">Content</Card>);
expect(screen.getByText('Content').closest('.card')).toHaveClass('custom-card');
});
test('applies border when bordered prop is true', () => {
render(<Card bordered>Content</Card>);
expect(screen.getByText('Content').closest('.card')).toHaveClass('border');
});
});
4. Komponenten mit Context testen
Für Komponenten, die Context verwenden (z.B. Theme-Context):
// ThemeAwareComponent.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '@smolitux/theme';
import { ThemeAwareComponent } from '../ThemeAwareComponent';
describe('ThemeAwareComponent', () => {
test('renders with light theme by default', () => {
render(
<ThemeProvider>
<ThemeAwareComponent />
</ThemeProvider>
);
expect(screen.getByTestId('theme-component')).toHaveClass('bg-white');
});
test('renders with dark theme when provided', () => {
render(
<ThemeProvider initialTheme="dark">
<ThemeAwareComponent />
</ThemeProvider>
);
expect(screen.getByTestId('theme-component')).toHaveClass('bg-gray-800');
});
});
5. Testen von seiteneffektbasierten Komponenten (useEffect)
Für Komponenten, die useEffect für Seiteneffekte verwenden:
// DataFetcher.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { DataFetcher } from '../DataFetcher';
// Mock der fetch API
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test data' }),
})
) as jest.Mock;
describe('DataFetcher', () => {
test('shows loading state initially', () => {
render(<DataFetcher url="https://example.com/data" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('shows data after successful fetch', async () => {
render(<DataFetcher url="https://example.com/data" />);
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('test data')).toBeInTheDocument();
});
expect(global.fetch).toHaveBeenCalledWith('https://example.com/data');
});
test('shows error message on fetch failure', async () => {
// Überschreiben des Mocks für diesen Test
global.fetch = jest.fn(() =>
Promise.reject(new Error('Failed to fetch'))
) as jest.Mock;
render(<DataFetcher url="https://example.com/data" />);
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('Error: Failed to fetch')).toBeInTheDocument();
});
});
});
6. Testkomponenten für komplexe Szenarien erstellen
Für komplexe Komponenten kann es hilfreich sein, eine Testkomponente zu erstellen:
// ComplexComponent.test.tsx
import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ComplexComponent } from '../ComplexComponent';
// Testkomponente, die den internen Zustand verwaltet
const TestWrapper = () => {
const [value, setValue] = useState('');
return (
<ComplexComponent
value={value}
onChange={(newValue) => setValue(newValue)}
data-testid="complex-component"
/>
);
};
describe('ComplexComponent', () => {
test('renders and handles state changes correctly', () => {
render(<TestWrapper />);
const component = screen.getByTestId('complex-component');
expect(component).toBeInTheDocument();
// Weiter mit Tests für interne Zustandsänderungen...
});
});
7. Test-Mocks für externe Abhängigkeiten
Für Komponenten, die externe Dienste oder Bibliotheken verwenden:
// Mocks erstellen
jest.mock('@smolitux/utils', () => ({
formatDate: jest.fn((date) => '01/01/2023'),
validateEmail: jest.fn((email) => email.includes('@')),
}));
// Im Test verwenden
import { formatDate, validateEmail } from '@smolitux/utils';
describe('FormattedDate', () => {
test('uses formatDate utility correctly', () => {
render(<FormattedDate date={new Date()} />);
expect(formatDate).toHaveBeenCalled();
expect(screen.getByText('01/01/2023')).toBeInTheDocument();
});
});
8. Test-Helpers für wiederkehrende Patterns
Für häufig verwendete Test-Patterns:
// test-utils.ts
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider } from '@smolitux/theme';
// Custom render mit ThemeProvider
const customRender = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'> & { theme?: 'light' | 'dark' }
) => {
const { theme = 'light', ...rest } = options || {};
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider initialTheme={theme}>{children}</ThemeProvider>
),
...rest,
});
};
// Im Test verwenden
import { customRender } from '../../test-utils';
test('renders with theme', () => {
customRender(<ThemeAwareComponent />, { theme: 'dark' });
// Tests...
});