Frontend-Tests
Teste, was der Benutzer sieht und tut, nicht wie die Komponente aufgebaut ist. Kombiniere mit naki-tdd für den Entwicklungszyklus, naki-e2e-playwright für vollständige Browserabläufe und naki-accessibility für Semantik. Befolge das Projektregeln-Skill, falls vorhanden (z. B. naki-myapp).
Wann verwenden
- Eine neue Komponente, ein neuer Hook oder eine neue Seite benötigt Tests.
- Ein Test bricht nach einem Refactoring, das das Verhalten nicht geändert hat (Anzeichen für Kopplung an die Implementierung).
- Tests sind flaky, verwenden überall
waitForoder mocken die halbe App.
Kernregeln
- Selektiere wie ein Benutzer. Priorität:
getByRole(mitname),getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. VerwendegetByTestIdnur, wenn nichts zugängliches existiert. - Interagiere mit
userEvent, nicht mitfireEvent. Erstelle es mitconst user = userEvent.setup()und warte auf jeden Aufruf mitawait. - Prüfe die Ausgabe: sichtbaren Text, Rollen, Formularwerte, Aufrufe eines Netzwerkendpunkts. Prüfe niemals Zustandsvariablen, Hook-Interna, CSS-Klassennamen oder Instanzmethoden von Komponenten.
- Mocke das Netzwerk, nicht deine Module. Verwende MSW-Handler auf HTTP-Ebene. Mocke ein Modul nur für echte externe Grenzen (Zeit, Zufall, Browser-APIs, die nicht in jsdom sind).
- Verwende
findBy*für Dinge, die später erscheinen. ReservierewaitForfür Assertions, die keine Selektoren sind, und setze niemals Seiteneffekte inwaitFor. - Ein Verhalten pro Test, benannt als Satz:
zeigt einen Fehler an, wenn der Server 500 zurückgibt. - Jeder Test richtet seinen eigenen Zustand ein. Keine Reihenfolgenabhängigkeit; setze Handler, Timer und Speicher in
afterEachzurück. - Stelle echte Provider durch einen
renderWithProviders-Helfer bereit (Router, Query-Client mit deaktivierten Retries, Theme), anstatt sie zu mocken. - Barrierefreiheit ist testbar. Ein fehlschlagender
getByRole("button", { name: "Speichern" })weist oft auf einen echten Barrierefreiheitsfehler hin. Füge optionalvitest-axe-Prüfungen für wichtige Bildschirme hinzu. - Coverage ist ein Signal, kein Ziel. Deckt die Zweige ab, die wichtig sind: Fehlerzustände, leere Zustände, Berechtigungen.
Selektoren-Referenz
| Element | Selektor |
|---|---|
| Button | getByRole("button", { name: /speichern/i }) |
| Link | getByRole("link", { name: "Dokumentation" }) |
| Texteingabe mit Label | getByRole("textbox", { name: "E-Mail" }) oder getByLabelText("E-Mail") |
| Checkbox | getByRole("checkbox", { name: "Angemeldet bleiben" }) |
| Überschrift | getByRole("heading", { level: 2, name: "Benachrichtigungen" }) |
| Tabellenzeile | getAllByRole("row") dann within(row) |
| Dialog | getByRole("dialog", { name: "Benachrichtigung löschen" }) |
| Abwesenheit | expect(queryByText("Fehler")).not.toBeInTheDocument() |
Arbeitsablauf
- Liste die Verhalten auf: Render-Zustände (Laden, Leer, Daten, Fehler) und Interaktionen.
- Füge MSW-Handler für die Endpunkte hinzu, die die Komponente aufruft; Standard ist Erfolg.
- Schreibe einen Test pro Verhalten und überschreibe Handler mit
server.use(...)für Fehlerfälle. - Führe
npx vitest run path/to/file.test.tsxaus, dann die gesamte Suite. - Zerstöre die Implementierung absichtlich einmal (kommentiere eine Zeile aus), um zu bestätigen, dass der Test fehlschlagen kann.
Setup
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());
onUnhandledRequest: "error" lässt einen fehlenden Handler lautstark fehlschlagen, anstatt zu hängen.
Beispiel
import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";
test("listet Benachrichtigungen auf und filtert nach Schweregrad", async () => {
server.use(http.get("/api/alerts", () => HttpResponse.json([
{ id: "a1", title: "Brute-Force am VPN", severity: "high" },
{ id: "a2", title: "Neuer Admin-Login", severity: "low" },
])));
const user = userEvent.setup();
render(<AlertList />);
expect(await screen.findByText("Brute-Force am VPN")).toBeInTheDocument();
await user.selectOptions(screen.getByRole("combobox", { name: "Schweregrad" }), "high");
expect(screen.queryByText("Neuer Admin-Login")).not.toBeInTheDocument();
});
test("zeigt einen Wiederholen-Button, wenn das Laden fehlschlägt", async () => {
server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
render(<AlertList />);
expect(await screen.findByRole("button", { name: /wiederholen/i })).toBeInTheDocument();
});
Hooks und Zeit
- Teste einen Hook durch eine kleine Komponente, die ihn verwendet, oder mit
renderHook, wenn er kein UI hat. - Gefälschte Timer:
vi.useFakeTimers()unduserEvent.setup({ advanceTimers: vi.advanceTimersByTime }); stelle mitvi.useRealTimers()inafterEachwieder her. - Friere Daten ein mit
vi.setSystemTime(new Date("2026-01-01T00:00:00Z")), damit Snapshots und Labels stabil sind.
Checkliste
- Selektoren basieren auf Rollen oder Labels.
- Alle
userEvent-Aufrufe werden mitawaiterwartet. - Netzwerk mit MSW gemockt; unbehandelte Anfragen schlagen fehl.
- Lade-, Fehler- und leere Zustände abgedeckt.
- Keine Assertions auf Interna oder Klassennamen.
- Tests bestehen allein und in beliebiger Reihenfolge.
- Keine
act(...)-Warnungen in der Ausgabe.
Häufige Fehler
| Fehler | Behebung |
|---|---|
getByTestId überall | Rollen-/Label-Selektoren; füge Labels zur Komponente hinzu, falls sie fehlen |
await waitFor(() => getByText("x")) | await findByText("x") |
fireEvent.change zum Tippen | await user.type(input, "text") |
Direktes Mocken des useAlerts-Hooks | Mocke den HTTP-Endpunkt |
| Große Snapshots ganzer Seiten | Explizite Assertions auf das, was wichtig ist |
| Query-Client mit aktivierten Retries, langsame Fehlertests | retry: false im Test-Provider |
act-Warnungen ignoriert | Warte auf das asynchrone UI-Update mit findBy* |