Soft

Blog

Next.js-Architektur, Fehlerfälle und Benutzerabläufe testen

Zuverlässige Next.js-Tests bestehen nicht nur aus Komponenten-Snapshots und erfolgreichen API-Antworten. Eine produktionsreife Strategie schützt Architekturgrenzen, Fehlerbehandlung, Server- und Client-Verhalten sowie vollständige Benutzerabläufe.

By Damian Ostraszewski

Ein bestandener Komponententest beweist nicht, dass die Anwendung funktioniert

Eine Next.js-Anwendung kann Hunderte bestandene Unit-Tests haben und dennoch genau in dem Ablauf scheitern, auf den ein Kunde angewiesen ist.

Ein Button kann korrekt gerendert werden, während:

  • der Server Daten in einer unerwarteten Struktur zurückgibt,
  • die Session während des Absendens abläuft,
  • eine Route beim Rendern eine Exception wirft,
  • gecachte Daten nach einer Mutation veraltet bleiben,
  • eine Anfrage zweimal gesendet wird,
  • ein Redirect das ursprüngliche Ziel verliert,
  • eine Error Boundary hilfreiche Wiederherstellungsaktionen verbirgt,
  • eine reine Browser-API während des Server-Renderings fehlschlägt,
  • ein Ladezustand niemals endet,
  • ein optimistisches Update Daten anzeigt, die nicht gespeichert wurden.

Die Teststrategie muss deshalb mehr als Komponenten schützen.

Eine produktive Next.js-Anwendung sollte entlang dreier miteinander verbundener Dimensionen getestet werden:

Architektur
→ Sind Verantwortlichkeiten und Abhängigkeitsrichtungen korrekt?

Fehlerfälle
→ Verhält sich die Anwendung sicher, wenn Abhängigkeiten ausfallen?

Benutzerabläufe
→ Kann ein Benutzer ein wichtiges Ziel über die reale Oberfläche erreichen?

Diese Dimensionen getrennt zu testen ist nützlich.

Zu testen, wie sie zusammenspielen, schützt die Anwendung.

Mit dem Verhalten beginnen, von dem das Geschäft abhängt

Teams beginnen häufig mit der Frage:

Welchen Prozentsatz Testabdeckung sollten wir erreichen?

Eine bessere Frage lautet:

Welche Fehler würden einen Benutzer daran hindern, eine wichtige Aufgabe abzuschließen?

Betrachten wir einen Bestellablauf:

Produkt öffnen
→ Artikel in den Warenkorb legen
→ Checkout öffnen
→ Lieferdaten eingeben
→ Bestellung absenden
→ Zahlung autorisieren
→ Bestätigung anzeigen

Für das Geschäft ist es unerheblich, ob jede React-Komponente einen isolierten Snapshot besitzt.

Entscheidend ist, ob:

  • das richtige Produkt im Warenkorb landet,
  • Preise nicht im Browser verändert werden können,
  • die Bestellung nicht zweimal aufgegeben wird,
  • ein Zahlungsfehler behebbar ist,
  • eine abgelaufene Session nicht alle eingegebenen Daten vernichtet,
  • eine erfolgreiche Zahlung genau eine Bestellung erzeugt,
  • die Bestätigung die richtige Bestellreferenz anzeigt.

Der Ablauf sollte die Teststrategie bestimmen.

Eine nützliche Testlandkarte für Next.js

Eine praxisnahe Testarchitektur kann so aussehen:

Statische Prüfungen
    │
    ├── TypeScript
    ├── Linting
    └── Import- und Architekturregeln
    │
    ▼
Domain- und Anwendungstests
    │
    ├── Geschäftsberechnungen
    ├── Zustandsübergänge
    └── Orchestrierung von Use Cases
    │
    ▼
Komponententests
    │
    ├── Interaktion
    ├── Validierung
    ├── Ladezustände
    └── behebbare Fehler
    │
    ▼
Vertrags- und Integrationstests
    │
    ├── Backend-Antworten
    ├── Authentifizierung
    ├── Route Handler
    └── Cache-Invalidierung
    │
    ▼
Browser-Workflow-Tests
    │
    ├── Navigation
    ├── vollständige Formulare
    ├── Redirects
    └── kritische Abläufe
    │
    ▼
Verifikation in Produktion
        ├── Logs
        ├── Metriken
        ├── Traces
        └── synthetische Checks

Jede Ebene beantwortet eine andere Frage.

Nur Browser-Tests einzusetzen macht die Diagnose von Fehlern langsam.

Nur Unit-Tests einzusetzen lässt Integrationsfehler ungeschützt.

Eine CI-Pipeline sollte bei jeder Änderung schnelle deterministische Prüfungen ausführen, Browser-Testdaten isolieren und langsamere serviceübergreifende Abläufe als risikobasierte Release-Gates statt als optionale Diagnostik behandeln.

Die Architektur testen, bevor sie schwer zu ändern ist

Architekturtests verifizieren, von welchen Bereichen Code abhängen darf.

Ein wartbares Next.js-Projekt kann Grenzen wie diese verwenden:

app
→ Route-Komposition und Framework-Integration

features
→ benutzerorientierte Use Cases

domain
→ Geschäftstypen und -regeln

application
→ Orchestrierung und Ports

infrastructure
→ API-Clients, Storage und Provider-Adapter

ui
→ wiederverwendbare Präsentationskomponenten

Die konkreten Ordner sind weniger wichtig als die Abhängigkeitsrichtung.

Ein Feature sollte keine React-Komponente dazu zwingen, Folgendes zu kennen:

  • rohe Datenbankstrukturen,
  • Response-Objekte eines Provider-SDK,
  • Interna von Authentifizierungs-Cookies,
  • Fehlercodes eines Zahlungsanbieters,
  • Details der Cache-Implementierung.

Eine konzeptionelle Abhängigkeitsregel lautet:

UI
→ Anwendungsvertrag
→ Domain-Vertrag
← Infrastrukturadapter

Die Infrastruktur implementiert den Vertrag.

Die Anwendungsschicht hängt nicht vom Provider ab.

Beispiel: die Checkout-Abhängigkeit isolieren

Statt eine externe API direkt aus einer Komponente aufzurufen:

export function CheckoutForm(): JSX.Element {
    async function submit(): Promise {
        await fetch('/api/orders', {
            method: 'POST',
            body: JSON.stringify(formValues),
        });
    }

    return {/* ... */};
}

definieren wir einen projektspezifischen Vertrag:

export type CheckoutInput = Readonly;

export type CheckoutResult =
    | Readonly
    | Readonly
    | Readonly;

export interface CheckoutGateway {
    submit(input: CheckoutInput): Promise;
}

Ein Infrastrukturadapter besitzt die HTTP-Details:

export class HttpCheckoutGateway implements CheckoutGateway {
    public constructor(
        private readonly baseUrl: string,
        private readonly fetcher: typeof fetch,
    ) {}

    public async submit(
        input: CheckoutInput,
    ): Promise {
        const response = await this.fetcher(
            `${this.baseUrl}/orders`,
            {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                    'idempotency-key': input.idempotencyKey,
                },
                body: JSON.stringify(input),
            },
        );

        if (!response.ok) {
            throw await mapCheckoutFailure(response);
        }

        return validateCheckoutResult(await response.json());
    }
}

Tests können nun das Gateway ersetzen, ohne das Browser-Networking global auszutauschen.

Der Adapter kann fokussierte Vertragstests erhalten.

Die Komponente kann Verhaltenstests erhalten.

Der vollständige Ablauf kann weiterhin durch einen Browser-Test abgedeckt werden.

Architekturtests sollten reale Grenzen durchsetzen

Abhängig von den bereits im Projekt eingesetzten Werkzeugen lassen sich Architekturregeln prüfen durch:

  • Lint-Regeln für eingeschränkte Imports,
  • Abhängigkeitsanalyse,
  • TypeScript Project References,
  • Paketgrenzen,
  • fokussierte Quellcode-Tests.

Sinnvolle Regeln umfassen:

Domain-Code darf weder React noch Next.js importieren.
Application Use Cases dürfen keine Provider-SDKs importieren.
UI-Komponenten dürfen Infrastrukturimplementierungen nicht direkt importieren.
Feature-Module dürfen nicht auf interne Dateien anderer Features zugreifen.
Server-only-Module dürfen nicht in Client-Bundles importiert werden.
Route-Dateien sollten Abhängigkeiten komponieren, statt vollständige Geschäftsabläufe zu enthalten.

Regeln sollten nicht allein deshalb entstehen, weil sie sauber klingen.

Jede Regel sollte eine konkrete Fehlerklasse verhindern.

Beobachtbares Verhalten testen, nicht die interne Implementierung

Ein fragiler Komponententest behauptet:

Die Komponente hat setState dreimal aufgerufen.

Ein nützlicher Test behauptet:

Der Absende-Button ist während der laufenden Anfrage nicht verfügbar.
Eine fehlgeschlagene Anfrage zeigt einen behebbaren Fehler.
Die eingegebenen Werte des Benutzers bleiben erhalten.
Ein erfolgreiches Ergebnis navigiert zur Bestätigungsseite.

Tests sollten internes Refactoring überstehen, solange das sichtbare Verhalten unverändert bleibt.

Vermeiden Sie Assertions über:

  • Aufrufe privater Helper,
  • Implementierungsdetails von Hooks,
  • die exakte Tiefe des Komponentenbaums,
  • vollständige HTML-Snapshots komplexer Ansichten,
  • vom Framework erzeugte Klassennamen.

Prüfen Sie Ergebnisse und stabile Accessibility-Rollen.

Verantwortlichkeiten von Server und Client getrennt testen

Next.js-Anwendungen kombinieren häufig serverseitig gerenderte Daten mit Client-Interaktionen.

Eine sinnvolle Trennung lautet:

Verantwortung des Servers
→ Authentifizierung
→ initiales Laden der Daten
→ Autorisierung
→ stabile Seitenkomposition

Verantwortung des Clients
→ unmittelbare Interaktion
→ lokale Validierung
→ Pending-Zustand
→ Wiederholung
→ progressive Aktualisierungen

Der Server-Test sollte verifizieren, dass nicht autorisierte Benutzer keine geschützten Daten erhalten.

Der Komponententest sollte verifizieren, dass der Benutzer einen hilfreichen Zustand sieht, wenn eine clientseitige Mutation fehlschlägt.

Der Browser-Test sollte das vollständige Verhalten verifizieren.

Zwingen Sie nicht jede Bedingung durch einen teuren Browser-Test.

Fehlerfälle müssen Teil des Designs sein

Die meisten Tests beginnen mit erfolgreichen Antworten:

gateway.submit.mockResolvedValue({
    status: 'completed',
    orderId: 'order-123',
    confirmationNumber: 'ORD-2026-123',
});

Produktionsvorfälle beginnen üblicherweise anderswo:

Die API lief in ein Timeout.
Der Benutzer klickte zweimal.
Die Antwort war valides JSON, hatte aber eine ungültige Struktur.
Die Session lief ab.
Der Warenkorb änderte sich.
Der Zahlungsanbieter akzeptierte die Zahlung, aber der Redirect schlug fehl.
Der Cache zeigte alte Daten.

Für einen Ablauf sollte eine explizite Fehlermatrix existieren.

Beispiel einer Checkout-Fehlermatrix

Fehler                            Erwartetes Anwendungsverhalten
────────────────────────────────────────────────────────────────────────────
Produktanfrage schlägt fehl       Retry anzeigen, ohne die Route abstürzen zu lassen
Warenkorb ist leer                Redirect oder eindeutigen Leerzustand anzeigen
Warenkorb serverseitig geändert   Warenkorb aktualisieren und Bestätigung anfordern
Session abgelaufen                Anmeldung verlangen und Rücksprungziel erhalten
Checkout-Anfrage mit Timeout      Unklaren Zustand anzeigen; nicht blind erneut senden
Zahlung abgelehnt                 Bestellkontext erhalten und andere Methode erlauben
Antwortschema ungültig            Sicher fehlschlagen; Vertragsverletzung melden
Doppeltes Absenden                Idempotenzschlüssel wiederverwenden und Duplikat verhindern
Bestätigungsanfrage schlägt fehl  Wiederherstellung über Bestellreferenz ermöglichen
Veralteter gecachter Warenkorb    Nach akzeptierter Mutation invalidieren oder aktualisieren

Jede wichtige Zeile sollte auf der günstigsten zuverlässigen Testebene abgedeckt sein.

Ladeverhalten als Zustand testen, nicht als Screenshot

Ladeverhalten sollte folgende Fragen beantworten:

  • Was kann weiterhin benutzt werden?
  • Was ist deaktiviert?
  • Kann der Benutzer die Seite verlassen?
  • Bleibt das Layout stabil?
  • Was geschieht nach einer langsamen Antwort?
  • Gibt es einen Timeout- oder Retry-Pfad?
  • Wird der Fortschritt barrierefrei angekündigt?

Ein Komponententest kann beispielsweise verifizieren:

it('prevents duplicate checkout submission while pending', async () => {
    const deferred = createDeferred();
    const submit = vi.fn(() => deferred.promise);

    render();

    await userEvent.click(
        screen.getByRole('button', { name: /place order/i }),
    );

    expect(
        screen.getByRole('button', { name: /place order/i }),
    ).toBeDisabled();

    await userEvent.click(
        screen.getByRole('button', { name: /place order/i }),
    );

    expect(submit).toHaveBeenCalledTimes(1);

    deferred.resolve({
        status: 'completed',
        orderId: 'order-123',
        confirmationNumber: 'ORD-123',
    });
});

Passen Sie die Syntax an den Test Runner des Projekts an.

Das Beispiel nutzt React Testing Library mit Mocks im Vitest-Stil; ein Projekt mit Jest sollte dieselben Verhaltens-Assertions beibehalten, statt die Strategie nur wegen des Runners zu ändern.

Die entscheidende Assertion lautet, dass eine langsame Antwort keine doppelte Übermittlung erzeugt.

Behebbare Fehler explizit testen

Eine Fehlermeldung reicht nicht aus, wenn der Benutzer den Fehler nicht beheben kann.

Beispiel:

it('keeps entered delivery data after a recoverable failure', async () => {
    const submit = vi
        .fn()
        .mockRejectedValueOnce(new NetworkUnavailable())
        .mockResolvedValueOnce({
            status: 'completed',
            orderId: 'order-123',
            confirmationNumber: 'ORD-123',
        });

    render();

    await userEvent.type(
        screen.getByLabelText(/delivery note/i),
        'Leave with reception',
    );

    await userEvent.click(
        screen.getByRole('button', { name: /place order/i }),
    );

    expect(
        await screen.findByRole('alert'),
    ).toHaveTextContent(/could not submit/i);

    expect(
        screen.getByLabelText(/delivery note/i),
    ).toHaveValue('Leave with reception');

    await userEvent.click(
        screen.getByRole('button', { name: /try again/i }),
    );

    expect(submit).toHaveBeenCalledTimes(2);
});

Dieser Test schützt die Arbeit des Benutzers, nicht nur die Fehlermeldung.

Abgelehnte Geschäftsergebnisse von technischen Fehlern unterscheiden

Diese Ergebnisse sind nicht gleichwertig:

Zahlungsanbieter nicht verfügbar
Warenkorb enthält einen nicht verfügbaren Artikel
Benutzer besitzt keine Berechtigung
Backend hat fehlerhafte Daten zurückgegeben

Verwenden Sie explizite Ergebnistypen und Fehlerkategorien.

Eine geschäftliche Ablehnung kann als reguläres Ergebnis des Ablaufs gerendert werden:

if (result.status === 'rejected') {
    return showCheckoutCorrection(result.reason);
}

Ein technischer Fehler kann Folgendes erfordern:

  • Wiederholung,
  • Logging,
  • Incident-Metriken,
  • Fallback-Verhalten.

Wandeln Sie nicht jeden Fehler in ein generisches Something went wrong um.

Generische Meldungen können aus Sicherheitsgründen angemessen sein, die interne Klassifizierung sollte jedoch präzise bleiben.

Backend-Antworten an der Grenze validieren

TypeScript-Typen validieren kein JSON zur Laufzeit.

Dieser Code ist unsicher:

const result = await response.json() as CheckoutResult;

Die Assertion gibt lediglich dem Compiler eine Anweisung.

Sie prüft die Antwort nicht.

Verwenden Sie den im Projekt etablierten Ansatz zur Laufzeitvalidierung:

const payload: unknown = await response.json();

return checkoutResultSchema.parse(payload);

oder einen gleichwertigen projektspezifischen Validator.

Testen Sie:

  • fehlende Eigenschaften,
  • falsche Enum-Werte,
  • falsche Zahlenwerte,
  • unerwartete Nullwerte,
  • nicht unterstützte zusätzliche Daten, wenn relevant,
  • inkompatible Backend-Versionen.

Eine Vertragsverletzung sollte nahe am Adapter fehlschlagen, statt später einen scheinbar unabhängigen Komponentenfehler auszulösen.

Fehler auf Route-Ebene testen

Eine Route kann scheitern, bevor die interaktive Komponente gerendert wird.

Beispiele sind:

  • Fehler beim Ermitteln der Authentifizierung,
  • nicht verfügbarer Backend-Service,
  • ungültiger Route-Parameter,
  • fehlender Datensatz,
  • abgelehnte Autorisierung,
  • Timeout des Data Loaders,
  • unerwartetes Antwortschema.

Testen Sie das Route-Verhalten mit dem bestehenden Ansatz des Projekts.

Verifizieren Sie das vorgesehene Ergebnis:

Fehlender Datensatz
→ Not-found-Erlebnis

Nicht autorisierter Benutzer
→ Login- oder Access-denied-Verhalten

Temporärer Backend-Ausfall
→ behebbarer Route-Fehler

Ungültiger Route-Parameter
→ kontrollierte Antwort statt unbehandelter Exception

Verwenden Sie die im Projekt bereits vorhandenen Konventionen für Error, Loading und Not Found.

Erstellen Sie keine eigene Fehler-Infrastruktur, wenn die Anwendung bereits ein etabliertes Muster besitzt.

Error Boundaries sollten Wiederherstellung ermöglichen

Eine Error Boundary sollte nicht nur Folgendes anzeigen:

Something went wrong.

Sie sollte folgende Fragen beantworten:

  • Was ist fehlgeschlagen?
  • Ist der Fehler temporär?
  • Kann der Vorgang wiederholt werden?
  • Ist die Navigation weiterhin verfügbar?
  • Wurden Benutzereingaben erhalten?
  • Erzeugt ein Retry eine doppelte Aktion?
  • Wird der Fehler sicher gemeldet?

Ein sinnvoller Test auf Route-Ebene verifiziert:

Erste Anfrage wirft eine Exception
→ Fehleroberfläche erscheint
→ Benutzer aktiviert Retry
→ zweite Anfrage ist erfolgreich
→ erwarteter Seiteninhalt erscheint

Prüfen Sie keine internen Reset-Funktionen des Frameworks, wenn dasselbe Verhalten über die Oberfläche verifiziert werden kann.

Authentifizierung muss während des Ablaufs getestet werden

Nur den anfänglichen Redirect einer geschützten Seite zu testen reicht nicht aus.

Sessions können ablaufen, während der Benutzer:

  • ein langes Formular bearbeitet,
  • auf die Zahlung wartet,
  • eine Datei hochlädt,
  • mehrere Onboarding-Schritte durchläuft,
  • die Anwendung in einem anderen Tab geöffnet lässt.

Ein realistischer Test sollte Folgendes abdecken:

Benutzer öffnet den authentifizierten Checkout
→ gibt Daten ein
→ Session läuft ab
→ Absenden liefert Unauthorized
→ Benutzer wird zur Authentifizierung aufgefordert
→ Anwendung erhält ein sicheres Rücksprungziel
→ Ablauf kann ohne doppelte Übermittlung fortgesetzt werden

Bewahren Sie keine Secrets oder unsicheren Request Bodies in Redirect-URLs auf.

Die richtige Wiederherstellungsstrategie hängt von der Sensibilität der Daten ab.

Autorisierungstests gehören an die Servergrenze

Einen Button auszublenden ist keine Autorisierung.

Testen Sie, dass nicht autorisierte Anfragen auch dann abgelehnt werden, wenn sie manuell konstruiert wurden.

Beispiel:

Benutzer aus Tenant A fordert eine Bestellung aus Tenant B an
→ Server verweigert den Zugriff
→ geschützte Daten sind nicht in der Antwort enthalten
→ Ereignis wird gemäß Projektrichtlinie protokolliert

Der Browser-Test kann das sichtbare Verhalten prüfen.

Ein Integrationstest auf niedrigerer Ebene sollte die tatsächliche Autorisierungsgrenze verifizieren.

Navigation und Redirects testen

Redirect-Fehler treten häufig auf, wenn:

  • Locale-Präfixe verloren gehen,
  • Query-Parameter verschwinden,
  • Callback-URLs falsch codiert werden,
  • ein Authentifizierungs-Redirect in einer Schleife endet,
  • eine erfolgreiche Mutation zu veralteten Daten weiterleitet,
  • sich der Browser-Verlauf unerwartet verhält.

Verifizieren Sie für kritische Abläufe:

  • die Ziel-URL,
  • das erhaltene Locale,
  • den erhaltenen sicheren Rücksprungpfad,
  • das Verhalten von Zurück,
  • direkten Zugriff auf die Bestätigungsroute,
  • Aktualisieren der Seite nach Erfolg.

Testen Sie nicht jeden Link im Browser.

Konzentrieren Sie Browser-Tests auf Übergänge innerhalb eines Ablaufs.

Cache-Verhalten benötigt eigene Tests

Caching kann eine korrekte Mutation defekt erscheinen lassen.

Beispiel:

Benutzer aktualisiert die Lieferadresse
→ Backend speichert sie
→ Anwendung leitet zur Adressseite weiter
→ gecachte Seite zeigt die vorherige Adresse

Ein Test sollte den extern sichtbaren Vertrag verifizieren:

Nach einer akzeptierten Mutation zeigt der nächste relevante Lesezugriff den neuen Wert.

Je nach Projektarchitektur kann dies Folgendes erfordern:

  • Cache-Invalidierung,
  • Tag-Invalidierung,
  • Revalidierung eines Pfads oder einer Route,
  • Versionierung des Cache Keys,
  • Umgehen des Caches für benutzerspezifische Daten.

Verwenden Sie den im Projekt tatsächlich konfigurierten Cache-Mechanismus.

Testen Sie keine Framework-Interna.

Testen Sie, dass keine veralteten Daten angezeigt werden, wenn Aktualität erforderlich ist.

Globale Annahmen über den Cache vermeiden

Dieselben Daten können unterschiedliche Aktualitätsanforderungen haben:

Öffentliche Produktbeschreibung
→ darf kurzzeitig veraltet sein

Verfügbarer Bestand im Checkout
→ erfordert aktuelle serverseitige Validierung

Benutzerpräferenz im Profil
→ sollte eine akzeptierte Aktualisierung normalerweise sofort widerspiegeln

Finaler Zahlungsstatus
→ muss durch das maßgebliche Backend bestätigt werden

Tests sollten Geschäftsanforderungen widerspiegeln, nicht die pauschale Regel „Cache ist gut“ oder „Cache ist schlecht“.

Optimistische Updates gegen Ablehnung testen

Optimistische Oberflächen verbessern die Reaktionsgeschwindigkeit, erzeugen aber zusätzliche Zustände.

Beispiel:

Benutzer ändert die Artikelmenge von 1 auf 3
→ UI zeigt sofort 3
→ Server meldet, dass nur 2 verfügbar sind

Das erwartete Verhalten muss definiert sein:

Auf 1 zurücksetzen
oder
2 als akzeptiertes Maximum anzeigen
oder
den Benutzer bitten, den angepassten Warenkorb zu bestätigen

Testen Sie:

  • den optimistischen Zustand,
  • die akzeptierte Antwort,
  • die abgelehnte Antwort,
  • den Rollback,
  • die Fehlerankündigung,
  • Interaktion während des Pending-Zustands.

Lassen Sie das Rollback-Verhalten nicht implizit.

Browser-Tests sollten Abläufe abdecken, keine Implementierungsdetails

Ein Browser-Test, beispielsweise mit Playwright, sollte das Ziel des Benutzers beschreiben:

test('customer can complete checkout after correcting changed stock', async ({
    page,
}) => {
    await page.goto('/en/products/example-product');

    await page.getByRole('button', {
        name: /add to basket/i,
    }).click();

    await page.getByRole('link', {
        name: /checkout/i,
    }).click();

    await page.getByLabel(/delivery address/i)
        .selectOption('address-1');

    await page.getByRole('button', {
        name: /place order/i,
    }).click();

    await expect(
        page.getByRole('alert'),
    ).toContainText(/quantity has changed/i);

    await page.getByRole('button', {
        name: /accept changes/i,
    }).click();

    await expect(page).toHaveURL(/\/orders\/[^/]+\/confirmation/);

    await expect(
        page.getByRole('heading', {
            name: /order confirmed/i,
        }),
    ).toBeVisible();
});

Passen Sie Routen, Selektoren und Syntax an die reale Anwendung an.

Bevorzugen Sie barrierefreie Selektoren gegenüber CSS-Implementierungsdetails.

Backend-Zustand gezielt kontrollieren

Browser-Tests werden unzuverlässig, wenn sie von unkontrollierten gemeinsamen Daten abhängen.

Verwenden Sie einen etablierten Ansatz für Testdaten:

  • Test-Setup über eine API,
  • deterministische Fixtures,
  • isolierte Testdatenbank,
  • dedizierte Factories,
  • genehmigte Strategie zum Zurücksetzen der Datenbank.

Der Test sollte Folgendes kennen:

  • Produktbestand,
  • Benutzerberechtigungen,
  • Warenkorbzustand,
  • Backend-Fehlermodus,
  • erwartetes Bestellergebnis.

Verlassen Sie sich nicht auf produktionsähnliche Accounts, die von mehreren Tests gemeinsam verwendet werden.

Führen Sie kein destruktives Setup gegen eine reale Umgebung aus.

Fehler an kontrollierten Grenzen simulieren

Sinnvolle Punkte zur Fehlerinjektion sind:

API-Adapter
Authentifizierungsprovider
Payment Gateway
Data Loader
Datei-Upload-Service
Feature-Flag-Provider
Queue-Status-Endpunkt

Vermeiden Sie weitreichende Interceptions, die unbeabsichtigt andere Anfragen mocken.

Ein Test sollte den beabsichtigten Fehler deutlich machen:

await checkoutApi.respondOnce({
    status: 409,
    body: {
        code: 'basket_changed',
    },
});

Prüfen Sie anschließend das zugehörige Wiederherstellungsverhalten.

Nicht die vollständige Anwendung mocken

Übermäßiges Mocking erzeugt Tests, die lediglich bestätigen, dass die Mocks mit der Implementierung übereinstimmen.

Ein Komponententest kann sein Gateway ersetzen.

Ein Route-Integrationstest sollte mehr reales Routing- und Serialisierungsverhalten verwenden.

Ein Browser-Workflow-Test sollte das reale Frontend und eine kontrollierte Backend-Umgebung ausführen.

Wählen Sie die kleinste Grenze, die das beabsichtigte Risiko noch beweist.

Vertragstests zwischen Next.js und dem Backend einsetzen

Wenn Next.js eine Symfony-, Laravel- oder andere Backend-API konsumiert, müssen beide Seiten übereinstimmen bei:

  • HTTP-Status,
  • Authentifizierung,
  • Request-Struktur,
  • Response-Struktur,
  • Fehlercodes,
  • Nullwerten,
  • Datumsformaten,
  • Pagination,
  • Idempotenz,
  • Versionierung.

Vertragstests können basieren auf:

  • einem API-Schema,
  • gemeinsamen Fixtures,
  • aufgezeichneten und freigegebenen Beispielen,
  • Provider- und Consumer-Tests,
  • direkten Integrationstests.

Ein sinnvoller Test verifiziert mehr als eine 200-Antwort:

it('maps a completed checkout response', async () => {
    const fetcher = createFetchStub({
        status: 201,
        json: {
            status: 'completed',
            orderId: 'order-123',
            confirmationNumber: 'ORD-123',
        },
    });

    const gateway = new HttpCheckoutGateway(
        'https://backend.test',
        fetcher,
    );

    await expect(
        gateway.submit({
            basketId: 'basket-1',
            deliveryAddressId: 'address-1',
            idempotencyKey: 'checkout-attempt-1',
        }),
    ).resolves.toEqual({
        status: 'completed',
        orderId: 'order-123',
        confirmationNumber: 'ORD-123',
    });
});

Testen Sie auch die realen Fehlerstrukturen des Providers.

Doppelte Übermittlungen erfordern Idempotenz

Einen Button zu deaktivieren reduziert versehentliche Duplikate, bietet aber keinen vollständigen Schutz.

Duplikate können weiterhin entstehen durch:

  • Browser-Retries,
  • Netzwerk-Proxys,
  • Aktualisieren der Seite durch den Benutzer,
  • mehrere Tabs,
  • wiederholte Queue-Zustellung,
  • verzögerte Antworten.

Für folgenreiche Mutationen sollte das Backend einen Idempotenzschlüssel oder eine gleichwertige Geschäftsbedingung unterstützen.

Der Frontend-Test sollte verifizieren:

Ein Versuch des Benutzers
→ ein stabiler Idempotenzschlüssel
→ Retries verwenden denselben Schlüssel
→ ein neuer beabsichtigter Versuch erhält einen neuen Schlüssel

Das Backend bleibt für die Durchsetzung der Idempotenz verantwortlich.

Race Conditions gezielt testen

Eine häufige Race Condition entsteht, wenn der Benutzer die Auswahl ändert, während eine frühere Anfrage noch läuft.

Beispiel:

Benutzer sucht nach „symfony“
→ Anfrage A startet
→ Benutzer ändert die Suche zu „nextjs“
→ Anfrage B startet
→ Anfrage B endet
→ Anfrage A endet später

Die Seite darf das neuere Ergebnis nicht durch veraltete Daten ersetzen.

Testen Sie eine Ausführung außerhalb der ursprünglichen Reihenfolge:

it('does not display an older search response', async () => {
    const first = createDeferred();
    const second = createDeferred();

    const search = vi
        .fn()
        .mockReturnValueOnce(first.promise)
        .mockReturnValueOnce(second.promise);

    render();

    await userEvent.type(
        screen.getByRole('searchbox'),
        'symfony',
    );

    await userEvent.clear(
        screen.getByRole('searchbox'),
    );

    await userEvent.type(
        screen.getByRole('searchbox'),
        'nextjs',
    );

    second.resolve(resultFor('nextjs'));

    expect(
        await screen.findByText('Next.js result'),
    ).toBeVisible();

    first.resolve(resultFor('symfony'));

    expect(
        screen.queryByText('Symfony result'),
    ).not.toBeInTheDocument();
});

Verwenden Sie Cancellation, Request Identity oder ein State Management, das zur Implementierung passt.

Hydration und Unterschiede zwischen Umgebungen beachten

Code kann bei einer Client-Navigation funktionieren und bei einem direkten Seitenaufruf scheitern.

Häufige Ursachen sind:

  • Zugriff auf window während der Serverausführung,
  • unterschiedliche Verwendung der aktuellen Zeit auf Server und Client,
  • Unterschiede bei der Locale-Formatierung,
  • Zufallswerte während des Renderings,
  • Zugriff auf Browser Storage vor dem Mounting,
  • abweichende Feature Flags auf Server und Client.

Schützen Sie wichtige Seiten durch Tests, die Folgendes umfassen:

Direkte Navigation
Clientseitige Navigation
Refresh
Zurück und Vorwärts
Unterschiedliche Locales
Authentifizierter und anonymer Zustand

Unterdrücken Sie Hydration-Warnungen nicht, ohne die Abweichung zu finden.

Leere, partielle und unerwartete Daten testen

Produktionsdaten sind selten so vollständig, wie Fixtures vermuten lassen.

Testen Sie:

  • leere Listen,
  • fehlende optionale Bilder,
  • gelöschte verknüpfte Datensätze,
  • nullable Profilfelder,
  • nicht unterstützte Locales,
  • partielle Berechtigungen,
  • unbekannte Enum-Werte,
  • ältere Backend-Datensätze,
  • sehr lange Texte,
  • Null- und Maximalwerte.

Eine Seite sollte nicht abstürzen, weil eine optionale Relation fehlt.

Gleichzeitig dürfen erforderliche Geschäftsdaten nicht stillschweigend optional werden, nur damit ein Test besteht.

Barrierefreiheit gehört in Workflow-Tests

Ein Ablauf ist unvollständig, wenn er nur mit der Maus funktioniert.

Komponenten- und Browser-Tests sollten relevantes Verhalten verifizieren für:

  • barrierefreie Namen,
  • Formular-Labels,
  • Fokusbewegung,
  • Tastaturaktivierung,
  • Ankündigung von Validierungsfehlern,
  • Fehlerzusammenfassungen,
  • Fokus in modalen Dialogen,
  • Ankündigung von Ladezuständen.

Verifizieren Sie bei einer fehlgeschlagenen Formularübermittlung, dass der Benutzer den Fehler finden und fortfahren kann.

Verlassen Sie sich nicht allein auf visuelle Screenshots.

Visuelle Tests sind nützlich, aber begrenzt

Visuelle Regressionstests können Folgendes schützen:

  • größere Layout-Änderungen,
  • fehlende responsive Bereiche,
  • defekte Loading Skeletons,
  • Overflow,
  • falsche Themes.

Sie beweisen nicht:

  • Autorisierung,
  • korrekte API-Daten,
  • Idempotenz,
  • erfolgreiche Übermittlung,
  • barrierefreie Benennung,
  • Retry-Verhalten.

Nutzen Sie visuelle Tests als eine Ebene, nicht als Ersatz für Verhaltens-Assertions.

Responsives Workflow-Verhalten testen

Ein Checkout, der auf dem Desktop funktioniert, kann auf Mobilgeräten scheitern, weil:

  • der Absende-Button hinter einem fixierten Element verborgen ist,
  • die Tastatur Formularaktionen verdeckt,
  • eine Fehlerzusammenfassung außerhalb des Viewports liegt,
  • ein Dialog nicht scrollbar ist,
  • die Adressauswahl Hover voraussetzt,
  • sich die Navigation unerwartet schließt.

Verwenden Sie für kritische Abläufe eine kleine Zahl repräsentativer Viewport-Tests.

Duplizieren Sie nicht jeden Browser-Test über viele Dimensionen.

Wählen Sie Kombinationen risikobasiert aus.

Browser-Tests fokussiert und unabhängig halten

Ein Browser-Test sollte nicht davon abhängen, dass ein vorheriger Browser-Test Daten erstellt.

Bevorzugen Sie:

Kontrollierten Zustand vorbereiten
→ einen Ablauf ausführen
→ sein Ergebnis prüfen
→ über die etablierte Test-Infrastruktur aufräumen

Vermeiden Sie:

Test 1 erstellt Account
→ Test 2 erwartet diesen Account
→ Test 3 verändert denselben Account

Gemeinsam genutzte sequenzielle Abläufe erschweren die Fehlerdiagnose.

Die Test-Suite vor falscher Sicherheit schützen

Ein Test kann aus dem falschen Grund bestehen.

Beispiele:

  • Das Pattern für Request Interception wurde nie getroffen.
  • Eine Assertion prüfte ein veraltetes Element.
  • Der Test verwendete unerwartet eine gecachte Authentifizierung.
  • Das Formular wurde nicht übermittelt.
  • Der Browser-Test wurde wegen der Umgebungskonfiguration übersprungen.
  • Ein Fehler wurde durch einen zu breiten Exception Handler verborgen.
  • Der Test akzeptierte jeden Status unter 500.

Verwenden Sie Assertions, die beweisen, dass die wichtige Aktion stattgefunden hat.

Beispiel:

Request Payload verifizieren
Ergebnisroute verifizieren
Gespeicherten Backend-Zustand verifizieren
Sichtbare Bestätigung verifizieren

Nicht jeder Test benötigt alle vier Signale, kritische Abläufe sollten jedoch mehr als eines enthalten.

Produktionsverhalten vervollständigt die Teststrategie

Tests vor dem Release können nicht jede Kombination reproduzieren aus:

  • Browser,
  • Netzwerk,
  • Datenvolumen,
  • Provider-Ausfall,
  • Deployment-Zeitpunkt,
  • Cache-Zustand,
  • Session-Dauer.

Die Verifikation in Produktion sollte Folgendes umfassen:

  • Fehlerrate von Routen,
  • fehlgeschlagene API-Aufrufe,
  • Abschlussrate von Workflows,
  • Abbruchpunkte im Checkout,
  • Schleifen bei Authentifizierungs-Redirects,
  • clientseitige Exceptions,
  • Verletzungen von Backend-Verträgen,
  • Latenzperzentile,
  • Cache-Effektivität,
  • Erkennung doppelter Aktionen.

Eine nützliche Metrik lautet nicht nur:

Checkout API returned 201.

Sondern:

Benutzer hat Checkout geöffnet
→ Bestellung abgesendet
→ bestätigten Bestellzustand erreicht

Messen Sie den Abschluss von Geschäftsabläufen, soweit Datenschutz- und Monitoring-Richtlinien dies erlauben.

Korrelationskennungen hinzufügen

Ein Workflow kann folgende Systeme durchlaufen:

Browser
→ Next.js-Server
→ Backend-API
→ Zahlungsanbieter
→ Message Queue

Ohne Korrelationskennung erscheint ein Fehler als mehrere voneinander unabhängige Log-Einträge.

Verwenden Sie eine etablierte Request- oder Workflow-Kennung, die sicher weitergegeben werden kann.

Geben Sie sensible interne Kennungen nicht unnötig preis.

Tests können verifizieren, dass die Kennung:

  • an der richtigen Grenze erzeugt oder akzeptiert wird,
  • an das Backend gesendet wird,
  • in sicheren Logs enthalten ist,
  • an geeigneter Stelle zurückgegeben wird,
  • innerhalb eines Ablaufs nicht unvorhersehbar neu erzeugt wird.

Eine risikobasierte Testmatrix verwenden

Nicht jede Seite benötigt dieselbe Testtiefe.

Klassifizieren Sie Abläufe:

Kritisch
→ Zahlung, Authentifizierung, Berechtigungen, irreversible Änderungen

Wichtig
→ Veröffentlichung von Inhalten, Kontoeinstellungen, Dokument-Upload

Unterstützend
→ Filterung, optionale Einstellungen, Informationsseiten

Ein kritischer Ablauf kann Folgendes benötigen:

  • Unit-Tests,
  • Komponenten-Fehlertests,
  • Adapter-Vertragstests,
  • Autorisierungstests,
  • Browser-Erfolgstest,
  • Browser-Wiederherstellungstest,
  • Produktionsmonitoring.

Ein unterstützendes Feature benötigt möglicherweise nur fokussierte Komponenten- und Integrationsabdeckung.

Das Ziel ist nicht identische Abdeckung.

Das Ziel ist angemessener Schutz.

Eine praxisnahe Implementierungsreihenfolge

Phase 1: Den Ablauf abbilden

Dokumentieren Sie:

  • den Ausgangspunkt,
  • das Ziel des Benutzers,
  • Serverabhängigkeiten,
  • Client-Interaktionen,
  • den finalen maßgeblichen Zustand.

Phase 2: Die Fehlermatrix erstellen

Listen Sie auf:

  • Validierungsfehler,
  • Autorisierungsfehler,
  • Ausfälle von Abhängigkeiten,
  • Timeout-Zustände,
  • doppelte Aktionen,
  • veraltete Daten,
  • Teilerfolge.

Definieren Sie für jeden Fall die erwartete Wiederherstellung.

Phase 3: Architekturgrenzen bestätigen

Identifizieren Sie:

  • Anwendungsverträge,
  • Infrastrukturadapter,
  • Server-only-Module,
  • Client-Komponenten,
  • Laufzeitvalidierung von Antworten.

Fügen Sie durchsetzbare Importregeln hinzu, sofern das Projekt diese bereits unterstützt.

Phase 4: Tests auf niedrigerer Ebene hinzufügen

Testen Sie:

  • Domain-Regeln,
  • Ergebnistypen von Use Cases,
  • Response Mapping,
  • Fehlerklassifizierung,
  • Verhalten von Idempotenzschlüsseln.

Phase 5: Komponenten-Verhaltenstests hinzufügen

Testen Sie:

  • Validierung,
  • Pending-Zustand,
  • Verhinderung von Duplikaten,
  • behebbare Fehler,
  • erhaltene Benutzereingaben,
  • optimistisches Rollback.

Phase 6: Integrationstests hinzufügen

Testen Sie:

  • Authentifizierung,
  • Autorisierung,
  • Route-Antworten,
  • API-Verträge,
  • Cache-Invalidierung,
  • Redirects.

Phase 7: Kritische Browser-Abläufe hinzufügen

Decken Sie ab:

  • einen erfolgreichen Ablauf,
  • einen wichtigen Wiederherstellungsablauf,
  • einen Autorisierungs- oder Session-Ablauf-Fall, sofern das Risiko dies rechtfertigt.

Phase 8: Produktionssignale hinzufügen

Messen Sie:

  • Workflow-Abschluss,
  • technische Fehlerrate,
  • Erfolg der Wiederherstellung,
  • Latenz,
  • Vertragsverletzungen,
  • doppelte Aktionen.

Worauf Code Reviews achten sollten

Stellen Sie beim Review einer Next.js-Änderung folgende Fragen:

  • Werden Server-only-Daten von Client-Code importiert?
  • Ruft die Komponente Infrastruktur direkt auf?
  • Werden API-Daten zur Laufzeit validiert?
  • Werden Autorisierungsprüfungen auf dem Server durchgesetzt?
  • Was geschieht bei einem Timeout der Abhängigkeit?
  • Kann der Benutzer sicher wiederholen?
  • Kann die Mutation zweimal übermittelt werden?
  • Bleibt der Idempotenzschlüssel über Retries stabil?
  • Werden akzeptierte Daten invalidiert oder aktualisiert?
  • Können veraltete Anfragen einen neueren Zustand überschreiben?
  • Verhält sich direkte Navigation wie Client-Navigation?
  • Sind Lade-, Leer- und Fehlerzustände barrierefrei?
  • Schützt der Test Verhalten oder Implementierungsdetails?
  • Ist der vollständige wichtige Ablauf irgendwo abgedeckt?
  • Macht Produktionsmonitoring einen Workflow-Fehler sichtbar?

Regeln für KI-gestützte Entwicklung

Ein KI-Coding-Assistent sollte nicht nur folgende Aufgabe erhalten:

Add tests for this page.

Geben Sie ihm explizite Verantwortlichkeiten:

Inspect the page, application contract, API adapter and existing tests.

Identify:
- success states,
- loading states,
- business rejections,
- technical failures,
- authentication changes,
- duplicate submission risks,
- cache invalidation,
- complete user workflow.

Use the existing test runner and browser framework.
Do not add packages.
Do not replace project-owned interfaces with direct fetch calls.
Do not assert private implementation details.
Add the smallest test layer that proves each risk.

Verifizieren Sie vor der Übernahme KI-generierter Tests, dass sie beim beabsichtigten Defekt fehlschlagen können.

Ein Test, der bei defektem Verhalten nicht fehlschlagen kann, ist Dokumentation, kein Schutz.

Fazit aus Senior-Engineering-Perspektive

Beim Testen einer Next.js-Anwendung geht es nicht darum, möglichst viele Tests zu erzeugen.

Das Ziel besteht darin, Evidenz dafür zu schaffen, dass die Architektur verständlich bleibt, Fehler kontrolliert bleiben und Benutzer wichtige Arbeit abschließen können.

Eine ausgereifte Strategie schützt normalerweise:

  • die Abhängigkeitsrichtung,
  • Verantwortlichkeiten von Server und Client,
  • API-Verträge zur Laufzeit,
  • Authentifizierung und Autorisierung,
  • Ladeverhalten und Fehlerbehebung,
  • idempotente Mutationen,
  • Caching und Aktualität,
  • Race Conditions,
  • kritische Browser-Abläufe,
  • den Abschluss von Produktions-Workflows.

Die stärkste Test-Suite versucht nicht, jede mögliche Kombination im Browser auszuführen.

Sie platziert jedes Risiko auf der niedrigsten zuverlässigen Ebene und verbindet diese Ebenen durch eine kleine Zahl realistischer Benutzerabläufe.

Das macht die Anwendung sicher refaktorierbar.

Es macht auch KI-gestützte Entwicklung sicherer. Ein KI-Werkzeug kann Komponenten, Adapter und Tests schnell erzeugen, aber das Projekt muss bereits die Grenzen, Fehlererwartungen und Abläufe definieren, die generierter Code bewahren muss.

Eine erfolgreiche Anfrage ist nicht dasselbe wie ein erfolgreicher Benutzerablauf.

Eine gerenderte Seite ist nicht dasselbe wie eine geschützte Architektur.

Eine bestandene Test-Suite ist nur dann wertvoll, wenn sie bei den Problemen fehlschlägt, vor denen das Geschäft tatsächlich geschützt werden muss.