Die RTL-Philosophie: Test wie der Benutzer
React Testing Library (RTL) folgt einem einzigen Kernprinzip: "The more your tests resemble the way your software is used, the more confidence they can give you." Das bedeutet: Kein Zugriff auf interne State-Variablen, keine Enzyme-Shallow-Renders, keine Implementierungsdetails — nur das, was ein echter Nutzer sieht und tut.
Falsch: wrapper.state('isLoading') — testet Implementierung
Richtig: screen.getByRole('progressbar') — testet, was der Nutzer sieht
Claude Code versteht diese Philosophie und generiert Tests, die gegen die öffentliche Schnittstelle testen: ARIA-Rollen, sichtbarer Text, zugängliche Labels — genau das, worauf Screenreader und echte Nutzer angewiesen sind.
Setup: RTL mit Claude Code in 60 Sekunden
Starte ein neues Projekt und lass Claude Code das Testing-Setup übernehmen:
# Claude Code Prompt: "Richte React Testing Library mit Vitest und @testing-library/user-event@14 ein. Erstelle eine test-utils.tsx mit custom render-Wrapper für Provider." # Installierte Pakete (Claude Code führt das aus): npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom vitest jsdom
// test-utils.tsx — von Claude Code generiert import { render, RenderOptions } from '@testing-library/react' import { ReactElement } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const AllProviders = ({ children }: { children: React.ReactNode }) => { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) return <QueryClientProvider client={qc}>{children}</QueryClientProvider> } const customRender = (ui: ReactElement, options?: RenderOptions) => render(ui, { wrapper: AllProviders, ...options }) export * from '@testing-library/react' export { customRender as render }
render, screen, userEvent: Die wichtigsten APIs
render — Komponente ins DOM bringen
render() mountet die Komponente in ein echtes DOM-Environment (jsdom).
Claude Code bevorzugt den custom render aus test-utils, damit alle Provider vorhanden sind.
import { render, screen } from '../test-utils' import { LoginForm } from './LoginForm' describe('LoginForm', () => { it('zeigt Fehler bei leerem Submit', async () => { render(<LoginForm />) // screen = globales Abfrage-Objekt — kein wrapper.find() mehr! await userEvent.click(screen.getByRole('button', { name: /einloggen/i })) expect(screen.getByText(/E-Mail ist erforderlich/i)).toBeInTheDocument() }) })
screen — Queries wie ein Benutzer
screen bietet alle Query-Methoden. Die Priorität laut RTL-Docs:
| Priorität | Query | Wann nutzen |
|---|---|---|
| 1 (höchste) | getByRole | Buttons, Links, Headings, Inputs |
| 2 | getByLabelText | Formularfelder mit Label |
| 3 | getByPlaceholderText | Inputs ohne Label (notfalls) |
| 4 | getByText | Sichtbarer Text im DOM |
| 5 | getByTestId | Letzter Ausweg — data-testid |
getByRole gegenüber getByTestId bevorzugen. Falls keine
ARIA-Rolle passt, schlägt Claude Code vor, die Komponente zugänglicher zu machen.
userEvent — Echte Browser-Interaktionen
userEvent aus @testing-library/user-event@14 simuliert
echte Browser-Events — inklusive keydown, keyup, Focus-Management und
Pointer-Events. Immer await verwenden:
import userEvent from '@testing-library/user-event' it('Nutzer tippt E-Mail und sendet Formular', async () => { const user = userEvent.setup() // setup() = beste Praxis in v14 render(<LoginForm onSubmit={mockSubmit} />) await user.type( screen.getByLabelText(/e-mail/i), 'test@example.com' ) await user.type( screen.getByLabelText(/passwort/i), 'geheim123' ) await user.click(screen.getByRole('button', { name: /einloggen/i })) expect(mockSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'geheim123' }) })
Async Testing: waitFor und findBy
Moderne React-Apps sind async — Datenabruf, Animationen, Debouncing. RTL bietet zwei Hauptwerkzeuge für asynchrone Assertions:
findBy* = waitFor + getBy kombiniert — für einzelne Elemente
waitFor = beliebige Assertion wiederholen bis sie passt oder Timeout
waitForElementToBeRemoved = warten bis Loading-Spinner weg ist
// findBy* — Element taucht asynchron auf it('lädt Nutzerliste nach API-Call', async () => { render(<UserList />) // findByRole wartet automatisch (Standard: 1000ms) const listItems = await screen.findAllByRole('listitem') expect(listItems).toHaveLength(3) }) // waitFor — komplexere Assertions it('zeigt Erfolgsmeldung nach Submit', async () => { const user = userEvent.setup() render(<ContactForm />) await user.click(screen.getByRole('button', { name: /senden/i })) await waitFor(() => { expect(screen.getByText(/danke für deine nachricht/i)) .toBeInTheDocument() }, { timeout: 2000 }) }) // waitForElementToBeRemoved — Loading-State testen it('versteckt Spinner nach Laden', async () => { render(<Dashboard />) const spinner = screen.getByRole('progressbar') await waitForElementToBeRemoved(spinner) expect(screen.getByText(/willkommen zurück/i)).toBeVisible() })
act() manuell aufrufen ist in RTL fast nie nötig.
render, userEvent und die find*-Queries wrappen alles automatisch.
Claude Code gibt eine Warnung aus, wenn es in deinem Code unnötige act()-Wrapper entdeckt.
Custom Hooks testen mit renderHook
renderHook aus RTL ermöglicht das isolierte Testen von Custom Hooks — ohne eine Wrapper-Komponente bauen zu müssen. Claude Code nutzt es für alle Hook-spezifischen Tests:
- Hook mit
renderHook()rendern result.current= aktueller Rückgabewert- State-Änderungen via
act()triggern - Re-Renders:
rerender()mit neuen Props aufrufen
// useCounter.ts — der Hook export const useCounter = (initialValue = 0) => { const [count, setCount] = useState(initialValue) const increment = () => setCount(c => c + 1) const reset = () => setCount(initialValue) return { count, increment, reset } } // useCounter.test.ts — renderHook Test import { renderHook, act } from '@testing-library/react' import { useCounter } from './useCounter' describe('useCounter', () => { it('startet mit initialValue', () => { const { result } = renderHook(() => useCounter(5)) expect(result.current.count).toBe(5) }) it('increment erhöht den Zähler', () => { const { result } = renderHook(() => useCounter()) act(() => { result.current.increment() }) expect(result.current.count).toBe(1) }) it('reset kehrt zu initialValue zurück', () => { const { result } = renderHook(() => useCounter(10)) act(() => { result.current.increment() result.current.increment() result.current.reset() }) expect(result.current.count).toBe(10) }) it('reagiert auf Prop-Änderungen via rerender', () => { const { result, rerender } = renderHook( ({ init }) => useCounter(init), { initialProps: { init: 0 } } ) rerender({ init: 99 }) act(() => result.current.reset()) expect(result.current.count).toBe(99) }) })
Mock Service Worker (MSW): API-Mocks ohne Mocking-Framework
MSW (Mock Service Worker) ist der moderne Standard für API-Mocks in
React-Tests. Kein jest.mock('axios'), kein Monkey-Patching — MSW interceptiert echte
Fetch/XHR-Requests auf Netzwerkebene. Claude Code integriert MSW Handler direkt in Testdateien:
- Funktioniert mit fetch, axios, SWR, React Query — kein Setup pro Library
- Gleiche Handlers für Tests UND Browser-Development
- Requests bleiben realitätsnah — kein Implementierungs-Leak
- Claude Code kann Handler aus OpenAPI-Specs generieren
// msw/handlers.ts — geteilte API-Definitionen import { http, HttpResponse } from 'msw' export const handlers = [ http.get('/api/users', () => { return HttpResponse.json([ { id: 1, name: 'Anna Schmidt', role: 'admin' }, { id: 2, name: 'Max Müller', role: 'user' }, ]) }), http.post('/api/users', async ({ request }) => { const body = await request.json() return HttpResponse.json({ id: 3, ...body }, { status: 201 }) }), ] // vitest.setup.ts — Server einmal starten import { setupServer } from 'msw/node' import { handlers } from './msw/handlers' const server = setupServer(...handlers) beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) // Wichtig: Test-Isolation! afterAll(() => server.close()) // UserList.test.tsx — Error-State testen mit Override it('zeigt Fehlermeldung bei API-Fehler', async () => { server.use( http.get('/api/users', () => HttpResponse.json({ error: 'Verbindung unterbrochen' }, { status: 503 }) ) ) render(<UserList />) expect(await screen.findByText(/fehler beim laden/i)).toBeInTheDocument() })
Testing Best Practices: Was man NICHT testen sollte
Claude Code kennt die häufigsten Testing-Fehler und vermeidet sie aktiv. Diese Dinge solltest du nicht testen:
// FALSCH — testet interne State-Variable expect(component.state('isOpen')).toBe(true) // FALSCH — testet CSS-Klassenname (bricht bei Refactoring) expect(element).toHaveClass('dropdown--active') // RICHTIG — testet sichtbares Verhalten expect(screen.getByRole('listbox')).toBeVisible()
Die 5 Test-Verbote (Claude Code Checkliste)
- PropTypes/TypeScript-Typen — das ist der Compiler-Job
- Styling und CSS-Klassen — Screenshot-Tests oder Storybook dafür nutzen
- Third-Party-Libraries — React Query, Axios etc. sind bereits getestet
- Snapshot-Tests ohne Bedeutung — Snapshots bei jeder Änderung aktualisieren = Null-Aussage
- Private Methoden / interne Helpers — nur über Public API testen
Claude Code + RTL: Der komplette Workflow
So setzt Claude Code RTL in der Praxis ein — vom Prompt zum grünen Test:
# 1. Komponente beschreiben "Schreibe Tests für eine SearchBar-Komponente: - Debounced Input (300ms) - API-Call mit MSW mocken - Loading-State während Suche - Ergebnisliste mit getByRole('listitem') - Leerer Zustand wenn keine Treffer" # 2. Claude Code generiert automatisch: # - test-utils.tsx mit Providern # - msw/handlers.ts für /api/search # - SearchBar.test.tsx mit 8 Tests # - vitest.config.ts mit jsdom # 3. Ausführen npx vitest run → ✓ 8 Tests grün
Zusammenfassung: RTL mit Claude Code 2026
| Feature | Tool | Claude Code Nutzen |
|---|---|---|
| Komponenten rendern | render + custom wrapper | Generiert Provider-Setup automatisch |
| DOM abfragen | screen.getByRole | Bevorzugt ARIA-Queries, schlägt Zugänglichkeit vor |
| Nutzer-Interaktion | userEvent.setup() | Echte Events mit Keyboard + Focus |
| Async Elemente | findBy*, waitFor | Erkennt async Patterns im Komponentencode |
| Custom Hooks | renderHook | Isolierte Hook-Tests ohne Wrapper |
| API-Mocks | MSW http.get/post | Generiert Handler aus OpenAPI / Fetch-Calls |
React Testing Library zwingt dich, besser zugängliche Komponenten zu schreiben. Claude Code kennt diese Philosophie und generiert Tests, die auf Anhieb grün sind — und beim Refactoring stabil bleiben, weil sie Verhalten statt Implementierung testen.
Claude Code für dein React-Projekt nutzen
Tests schreiben, Bugs finden, Komponenten refactoren — Claude Code ist dein AI-Entwicklungspartner für moderne React-Stacks. Jetzt kostenlos testen.
Kostenlos starten →