← Zurück

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?

1.
getByRole

Accessibility-first. Wie Screen Reader navigiert.

getByRole('button', { name: /submit/i })
2.
getByLabelText

Für Form-Inputs. Wie User Labels lesen.

getByLabelText(/email/i)
3.
getByText

Für sichtbaren Text-Content.

getByText(/welcome/i)
4.
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