React Testing
Nicht darüber lesen — MACHEN.
🎯 Das Grundprinzip
"Test wie ein USER, nicht wie ein Developer."
Dein Test sollte nicht wissen, wie dein Code funktioniert. Er sollte wissen, was der USER sieht und tut.
Der Stack
Vitest
Test Runner — führt Tests aus, zeigt Ergebnisse
Schneller als Jest, native ESM
Testing Library
DOM Queries — findet Elemente wie ein User
getByRole, getByText, etc.
jest-dom
Matchers — bessere Assertions für DOM
toBeInTheDocument, toHaveTextContent
Setup (5 Minuten)
1. Dependencies installieren:
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
2. vite.config.ts erweitern:
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
}) 3. Setup-Datei erstellen (src/test/setup.ts):
import '@testing-library/jest-dom'
Query Priority — Was zuerst verwenden?
getByRole Accessibility-first. Wie Screen Reader navigiert.
getByRole('button', { name: /submit/i }) getByLabelText Für Form-Inputs. Wie User Labels lesen.
getByLabelText(/email/i)
getByText Für sichtbaren Text-Content.
getByText(/welcome/i)
getByTestId Letzter Ausweg. Wenn nichts anderes funktioniert.
getByTestId('custom-element') getBy vs findBy vs queryBy
getBy* Element MUSS sofort existieren.
Wirft Error wenn nicht gefunden.
findBy* Wartet auf Element (async).
Für Loading-States, API-Calls.
queryBy* Element kann fehlen.
Gibt null zurück, kein Error.
Merkregel: getBy = "muss da sein" |
findBy = "wird gleich da sein" |
queryBy = "ist vielleicht nicht da"
Echtes Beispiel: Button mit Counter
Component (Counter.tsx):
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
)
} Test (Counter.test.tsx):
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('increments count on click', async () => {
render(<Counter />)
// Initial state
expect(screen.getByText('Count: 0')).toBeInTheDocument()
// User interaction
await userEvent.click(
screen.getByRole('button', { name: /increment/i })
)
// Result
expect(screen.getByText('Count: 1')).toBeInTheDocument()
}) Form Validation testen
test('shows error when email is invalid', async () => {
const user = userEvent.setup()
render(<LoginForm />)
// Fill invalid email
await user.type(
screen.getByLabelText(/email/i),
'invalid-email'
)
// Submit
await user.click(
screen.getByRole('button', { name: /submit/i })
)
// Error should appear
expect(
screen.getByText(/please enter a valid email/i)
).toBeInTheDocument()
})
test('submits form with valid data', async () => {
const onSubmit = vi.fn()
const user = userEvent.setup()
render(<LoginForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
})
}) Async Components (API Calls)
test('loads and displays user data', async () => {
// Mock the API
vi.spyOn(api, 'fetchUser').mockResolvedValue({
name: 'John Doe',
email: 'john@example.com'
})
render(<UserProfile userId="123" />)
// Loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument()
// Wait for data - findBy wartet automatisch!
expect(
await screen.findByText('John Doe')
).toBeInTheDocument()
// Loading sollte weg sein
expect(
screen.queryByText(/loading/i)
).not.toBeInTheDocument()
}) Tipp: findBy* returned ein Promise und wartet bis zu 1000ms.
Perfekt für async Operations!
Häufige Fehler
❌ Implementation testen
// SCHLECHT: Testet State direkt expect(component.state.isOpen).toBe(true)
// GUT: Testet was User sieht
expect(screen.getByRole('dialog')).toBeVisible() ❌ getBy für nicht existierende Elemente
// SCHLECHT: Wirft Error wenn Element nicht da
expect(screen.getByText('Error')).not.toBeInTheDocument() // GUT: queryBy für "sollte nicht existieren"
expect(screen.queryByText('Error')).not.toBeInTheDocument() ❌ Sync Test für Async Code
// SCHLECHT: Wartet nicht auf async
screen.getByText('Loaded Data') // GUT: findBy wartet automatisch
await screen.findByText('Loaded Data') ✅ Was ich WIRKLICH verstanden habe
- • Teste wie USER — nicht wie Code funktioniert
- • getByRole first — Accessibility-driven Testing
- • getBy/findBy/queryBy — je nachdem ob Element da sein MUSS/WIRD/KANN
- • userEvent — simuliert echte User-Interaktionen
- • vi.fn() — mockt Funktionen um Calls zu verifizieren
- • vi.spyOn() — mockt API-Calls für async Tests