Blog
Teststrategie für Symfony und React: schnelles Feedback, Stabilität und Wartungskosten
Tests für Symfony und React nach Risiken auswählen, schneller Rückmeldung erhalten und sicher refaktorieren, ohne CI und Testpflege unnötig teuer zu machen.
Tests sollen Änderungen erleichtern
Ein Artikeleditor soll künftig Entwürfe ohne Titel speichern dürfen, für die Veröffentlichung aber vollständigen Inhalt verlangen. Symfony setzt die Regel durch, React erklärt sie dem Benutzer. Nach jeder Änderung die gesamte Browsersuite zu starten, zeigt irgendwann, ob der Ablauf noch funktioniert. Für einen falschen Ausdruck in einer Policy ist das ein teurer Diagnoseweg.
Der umgekehrte Kurzschluss kostet ebenfalls: Eine isolierte Regel ist geprüft, also werden Editor, API und Datenbank schon zusammenspielen. Eine durchdachte Strategie ordnet Risiken passende Tests zu und prüft zusätzlich die entscheidenden Verbindungen zwischen den Schichten.
Das erleichtert die tägliche Arbeit: kürzere Unterbrechungen, verständlichere Fehler beim Refactoring und weniger Regressionen bei den Benutzern. Tests brauchen allerdings Daten, Umgebungen und Pflege. Auch ihre Fehleranalyse kostet Entwicklungszeit. Diese Kosten sollen überschaubar bleiben, ohne die Aussagekraft zu verlieren. Die Beispiele sind hypothetisch und beschreiben keine GiSoft-Kundenprojekte.
Feedback-Geschwindigkeit ist eine Architekturentscheidung
Entscheidend ist die Zeit von einer Änderung bis zu einer brauchbaren Einschätzung ihrer Sicherheit. Dazu zählen auch Wartezeiten auf CI-Runner, Datenbankvorbereitung und die Untersuchung unklarer Fehler. Kommt das Ergebnis erst während der nächsten Aufgabe, muss der Entwickler zusätzlich den alten Kontext rekonstruieren.
Auf eine geänderte Berechnung sollten meist statische Analyse und gezielte Unit-Tests schnell antworten. Eine Repository-Änderung braucht die passenden Datenbanktests. Anschließend kann ein Browsertest den betroffenen Ablauf absichern. Wer zuerst alles startet, bezahlt den größten Vorbereitungsaufwand, bevor die einfachen Fragen beantwortet sind.
Messen Sie im eigenen Repository. Statische Analyse ist nicht immer schnell; ein kleiner Integrationstest kann günstiger sein als ein Unit-Test mit vielen Mocks. Beobachten Sie neben der Gesamtdauer auch die Zeit bis zum ersten verständlichen Fehler. Ein schneller Test nützt nur, wenn er den relevanten Defekt erkennen kann.
Den Test vom möglichen Fehler her auswählen
Teams benennen ihre Suiten unterschiedlich. Hilfreicher ist die Frage, welche echten Teile ausgeführt und welche ersetzt werden. Beschreiben Sie einen möglichen Fehler und wählen Sie den günstigsten Test, der ihn zuverlässig beobachten kann:
Möglicher Fehler Erste sinnvolle Testgrenze
Falsche Berechnung Unit: Eingaben und Ergebnis
Fehlerhafte Doctrine-Abfrage Integration: Testdatenbank
Fehlende API-Berechtigung Funktional: Anfrage und Wirkung
Eingabe nach Fehler verloren Komponente: Benutzerinteraktion
Geändertes Antwortformat Vertrag: Anbieter und Verbraucher
Defekter Gesamtablauf Browser: ausgewählter Prozess
Deployment nicht erreichbar Smoke: bereitgestellter EinstiegDas ist eine Orientierung, keine starre Zuordnung. Eine Berechtigungspolicy kann viele Unit-Tests und zusätzlich einen HTTP-Test benötigen, der ihre Verwendung durch die Route belegt. Höhere Ebenen prüfen die Verbindungen; sie müssen nicht jede Variante der unteren Ebene wiederholen.
Statische Prüfungen liefern früh Ergebnisse. PHPStan und TypeScript finden unvereinbare Typen, unsichere Annahmen über null und verletzte deklarierte Schnittstellen. Linting und ausdrückliche Importregeln können bestimmte Verwechslungen zwischen Client- und Servercode erkennen. Architekturtests sichern vereinbarte Abhängigkeitsrichtungen, etwa den Verzicht auf HTTP-Typen in fachlichen Policies. Sie prüfen nur den erfassten Code und die festgelegten Regeln, kein fachliches Laufzeitverhalten.
In Symfony Entscheidungen direkt und Infrastruktur real prüfen
Unit-Tests eignen sich für Wertobjekte, Berechnungen, Policies, Mapper und eigene Validierungsregeln. Bei einem einfachen Objekt ist der Weg von den Eingaben zur Fehlerursache kurz. Kernel und Datenbank sind dafür normalerweise unnötig.
Das Pest-Beispiel setzt eine illustrative PublishPostPolicy voraus. Ihre Methode nimmt Titel und Inhalt entgegen und verweigert die Veröffentlichung, wenn eines der Felder nach dem Entfernen äußerer Leerzeichen leer ist. Implementierung und Import sind ausgelassen; die Klasse ist weder Symfony-API noch vorhandener Projekthelfer. Die Syntax passt zum Pest-3-/PHPUnit-11-Stack des Repositorys:
it('verlangt Inhalt vor der Veröffentlichung', function (
string $title,
string $body,
bool $allowed,
): void {
$policy = new PublishPostPolicy();
expect($policy->canPublish(title: $title, body: $body))
->toBe($allowed);
})->with([
['', 'Article text', false],
['Title', '', false],
[' ', 'Article text', false],
['Title', 'Article text', true],
]);Der gültige Fall verhindert, dass eine Policy besteht, die jede Veröffentlichung verweigert. Geprüft wird hier die Vollständigkeit des Inhalts. Die Berechtigung des Aufrufers bleibt eine eigene serverseitige Verantwortung.
Integrationstests lohnen sich, wenn die Verbindung selbst fehleranfällig ist: Doctrine-Mapping und DQL, Containerkonfiguration, Serializer, Cache-Adapter oder Messenger-Routing. Verwenden Sie dafür die relevante echte Infrastruktur. Ein gemockter QueryBuilder zeigt nicht, ob ein Join Artikel vervielfacht. Eine isolierte Testdatenbank braucht eine passende Engine und ein passendes Schema; wichtige oder komplexe Abfragen haben Vorrang.
Funktionale API-Tests führen Anfragen durch Symfony aus. Sie prüfen Routing, Methoden, Zugangsdaten, Autorisierung, Decodierung, Validierung, öffentliche Antworten und ausgewählte Auswirkungen. Für die Veröffentlichung sind drei Ergebnisse relevant: anonyme Anfragen werden vertragsgemäß abgewiesen, bekannte Benutzer ohne Berechtigung ändern keinen Datensatz, berechtigte Benutzer können veröffentlichen. Aus 403 allein folgt keine erfolgreiche Authentifizierung; Entry Point und Konfiguration bestimmen die konkrete Antwort. Der Symfony-Testclient prüft weder den bereitgestellten Proxy noch einen echten Browser.
Der GiSoft-Artikel Symfony-APIs mit Pest testen behandelt diese HTTP-Beispiele ausführlich. Hier geht es um die Aufteilung: die Regel günstig prüfen und anschließend nachweisen, dass der Endpunkt sie anwendet. Ein Serializer- oder Repository-Test ersetzt diese Verbindung nicht.
In React Bedienung und Fehlerbehebung testen
Reine Funktionen, Reducer, Formatierer und Zustandsübergänge passen ebenfalls in Unit-Tests. Komponententests werden interessant, sobald Rendering und Interaktion zusammenwirken: Validierung, Ladezustände, gesperrte Schaltflächen, behebbarer Fehler und erfolgreicher Abschluss.
Das Repository nutzt bereits Vitest, React Testing Library und user-event sowie jest-dom-Matcher im Testsetup. Der Ausschnitt setzt einen beispielhaften DraftEditor voraus; seine Implementierung wird nicht mitgeliefert. Er erhält initialTitle und den asynchronen Callback saveDraft({ title }) und verwendet die gezeigten englischen Beschriftungen:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, it, vi } from 'vitest';
import { DraftEditor } from './DraftEditor';
it('behält bearbeiteten Text nach einem Speicherfehler', async () => {
const user = userEvent.setup();
const saveDraft = vi.fn().mockRejectedValue(new Error('Unavailable'));
render(<DraftEditor initialTitle="Release notes" saveDraft={saveDraft} />);
const title = screen.getByRole('textbox', { name: 'Title' });
await user.clear(title);
await user.type(title, 'Revised notes');
await user.click(screen.getByRole('button', { name: 'Save draft' }));
expect(await screen.findByRole('alert'))
.toHaveTextContent('Could not save');
expect(screen.getByRole('textbox', { name: 'Title' }))
.toHaveValue('Revised notes');
expect(screen.getByRole('button', { name: 'Save draft' })).toBeEnabled();
expect(saveDraft).toHaveBeenCalledWith({ title: 'Revised notes' });
});Der Test schützt ein konkretes Verhalten: Nach dem fehlgeschlagenen Speichern bleibt der bearbeitete Text erhalten und lässt sich erneut absenden. Hook-Zustand, private Helfer und Komponentenverschachtelung sind dafür unerheblich. Abfragen über Rolle und zugänglichen Namen drücken die Absicht meist besser aus als layoutabhängige CSS-Selektoren. Vollständige Barrierefreiheit weisen sie allein nicht nach.
Prüfen Sie den Ladezustand separat mit einem kontrollierten, zunächst offenen Promise, das Sie anschließend gezielt abschließen. Eine deaktivierte Schaltfläche verhindert gegebenenfalls einen weiteren Klick, beweist aber keine Backend-Idempotenz. Ein ausgeblendeter Veröffentlichungsbutton erzwingt keine Serverberechtigung. Große Seitensnapshots verdecken solche Unterschiede oft; kleine Snapshots können sinnvoll sein, wenn gerade die gerenderte Ausgabe der Vertrag ist.
Den Vertrag zwischen Symfony und Next.js prüfen
Vertragstests prüfen, ob Anbieter und Verbraucher sich über Statuscodes, Pflichtfelder, null, Datumsformate, Fehlercodes, Pagination und Authentifizierungsannahmen einig sind. Grundlage können ein Schema, Anbieterprüfungen oder auf beiden Seiten geprüfte Antwortbeispiele sein. Eine unabhängig erfundene Frontend-Fixture belegt nur Übereinstimmung mit dieser Fixture.
TypeScript beschreibt Erwartungen des kompilierten Codes. Eine Typzusicherung für decodiertes JSON validiert die Antwort nicht. Prüfen Sie am API-Adapter gültige Daten und eine relevante inkompatible Antwort mit der vorhandenen Laufzeitvalidierung. Ändert Symfony publishedAt von einer ISO-Zeichenkette in eine Zahl, muss der Verbraucher die vereinbarte Änderung verarbeiten oder kontrolliert ablehnen. Ein Schema erkennt keine formal richtige Antwort mit Daten eines fremden Benutzers.
Next.js ergänzt eine Ausführungsgrenze. Clientnavigation, vollständiger Seitenaufruf und Serverrendering können Zugangsdaten und Cache unterschiedlich nutzen. Ein serverseitiger Fetch übernimmt eingehende Browser-Cookies oder den Authorization-Header nicht automatisch. Prüfen Sie die vorgesehene Weitergabe, ohne beliebige Header durchzureichen. Reines Mapping bleibt separat testbar; serverspezifisches Verhalten braucht eine geeignete Next.js-Laufzeit.
Die Unterstützung für Server Components hängt von den Testwerkzeugen und ihren Versionen ab. Der Vitest-Leitfaden für Next.js 16 nennt asynchrone Server Components als nicht unterstützt und empfiehlt dafür E2E-Tests. Ein bestandener Client-Komponententest sagt daher nichts über diesen Serverpfad aus. Der GiSoft-Artikel Next.js-Architektur, Fehlerfälle und Benutzerabläufe testen behandelt diese Ausführungs- und Fehlergrenzen genauer.
Browsertests auf wichtige Verbindungen konzentrieren
Browser-/E2E-Tests können das Zusammenspiel von Navigation, Rendering, Zugangsdaten und Backend prüfen. Ihr Diagnosebereich ist entsprechend groß: Ein Fehler kann im Selektor, einer abgelaufenen Session, fehlenden Daten, dem Netzwerk oder einem Service liegen. Besonders wertvoll sind sie für kritische Kauf-, Anmelde- oder Veröffentlichungsabläufe und einen gezielten Wiederherstellungsfall mit hohem Schadenspotenzial.
Nutzen Sie den etablierten Browser-Runner. Playwright ist eine Möglichkeit; das Paketmanifest dieses Frontends deklariert derzeit allerdings keine konfigurierte Playwright-Suite. Ein Artikelbeispiel sollte keinen zweiten Teststack erzwingen.
Benennen Sie ersetzte Teile ausdrücklich. Ein Browsertest mit ausschließlich simulierten API-Antworten prüft den Browserablauf, nicht die echte Symfony-Anbindung. Verbinden Sie zumindest den relevanten kritischen Pfad mit kontrollierten Backend-Daten. Reguläre CI verwendet Anbieterdoubles; bewusste Prüfungen gegen Anbieter-Testumgebungen laufen getrennt. Echte Zahlungen und unkontrollierte externe Dienste gehören nicht in Routinetests.
Smoke-Tests beantworten eine kleinere Deployment-Frage: Antwortet der bereitgestellte Einstieg, und gelingt ein ausgewählter sicherer Lesezugriff oder eine wesentliche Abhängigkeitsprüfung? Sie ergänzen Anwendungstests und Monitoring, bestätigen aber nicht sämtliche Berechtigungen und Geschäftsregeln.
Regressionsbudgets schützen gefährdete Listenendpunkte vor steigender Abfragezahl, übergroßen Antworten oder ignorierten Seitenlimits. Leiten Sie Grenzen aus repräsentativen Daten ab und trennen Sie Setup und Messung. Eine begrenzte Abfragezahl ist kein Latenzbenchmark. Detaillierte N+1- und Performanceanalyse gehört in gezielte Tests und Profiling statt in Zeitmessungen jedes Browserablaufs.
CI auf verwertbare Ergebnisse ausrichten
Ein sinnvoller Einstieg sind Syntax, Typen und andere günstige Prüfungen. Danach folgen gezielte Unit- und Komponententests, Infrastruktur und API sowie ausgewählte Browserabläufe. Unabhängige Jobs dürfen parallel laufen. Manche Integrationstests sind günstig genug für einen sofortigen Start; Änderungen an gemeinsamen Bibliotheken können von Anfang an breitere Prüfungen verlangen.
Beobachten Sie sowohl Wartezeit als auch gesamten Runnerverbrauch. Mehr Parallelität kann die Laufzeit verkürzen und zugleich Infrastrukturkosten oder Konkurrenz um eine gemeinsame Datenbank erhöhen. Trennen Sie nötigenfalls Daten, Cache-Namensräume und Ports. Cachen Sie Abhängigkeiten passend zu den Lockfiles, nicht den veränderlichen Zustand vorheriger Tests.
Während der Entwicklung helfen Tests nahe an der Änderung. Vor dem Merge richtet sich der Umfang nach deren Auswirkungen: Eine Auswahl allein anhand geänderter Dateinamen übersieht möglicherweise gemeinsamen Code, Schemas oder Konfiguration. Behalten Sie breitere Prüfungen an einem passenden Kontrollpunkt. Sichern Sie bei Browserfehlern Traces und nützliche Logs ohne Geheimnisse. Wiederholungen können bei der Diagnose helfen; sie dürfen Unzuverlässigkeit nicht stillschweigend in ein scheinbar belastbares Ergebnis verwandeln.
Messen Sie Warteschlange, Setup, langsame Suiten, sporadische Fehler und Untersuchungsaufwand. Verbessern Sie zuerst den tatsächlichen Zeitverbraucher, bevor Sie weitere Runner kaufen oder wichtige Assertions entfernen. Weder eine universelle CI-Dauer noch ein festes Verhältnis von Unit- zu E2E-Tests belegt eine gute Strategie.
Wartungskosten gehören zum Entwurf
Ein Test ist wertvoll, wenn er bei einer relevanten Änderung anschlägt und die Ursache eingrenzt. Wiederholte Ausfälle wegen unbedeutender Markup-Änderungen oder gemeinsamer Fixtures kosten Zeit und untergraben Vertrauen. Diese Kosten gehören in die Architekturentscheidung, nicht ausschließlich in ein späteres Aufräumbudget.
Fixtures sollten klein sein und entscheidende Angaben sichtbar machen: Eigentümer, Rolle, Veröffentlichungszustand und Sprache. Verwenden Sie deterministische IDs sowie eine injizierte Uhr, wenn Zeit das Ergebnis beeinflusst. Isolieren Sie veränderlichen Zustand und vermeiden Sie Abhängigkeiten von der Testreihenfolge. Helfer dürfen Wiederholung entfernen, aber nicht Authentifizierung, Datenbankzugriffe oder die Begründung eines erwarteten Erfolgs verbergen.
Ersetzen Sie eine Abhängigkeit, wenn ihre kontrollierte Antwort benötigt wird. Lassen Sie sie real, wenn ihr Verhalten das Risiko ist. Zu viele Mocks können einen Servicetest bestehen lassen, obwohl Verdrahtung oder Persistierung fehlerhaft sind. Sie koppeln Tests zudem an Aufrufreihenfolge und Objektstruktur. Bevorzugen Sie beobachtbare Ergebnisse; prüfen Sie interne Aufrufzahlen nur, wenn genau diese Interaktion eine Anforderung ist.
Kaufen Sie denselben Nachweis nicht mehrfach. Bei einer Preisberechnung prüfen Unit-Tests Rundung und Grenzwerte, ein API-Test die Abbildung von Betrag und Währung, ein Browsertest den erfolgreichen Kauf. Jede Rundungsvariante im Browser zu wiederholen erhöht Setup- und Diagnosekosten, ohne das Zusammenspiel wesentlich besser abzusichern.
Tests für stabiles Verhalten sollten Änderungen an Klassen oder Komponentenzusammensetzung überwiegend überstehen. Ändert sich das Verhalten absichtlich, ändern Sie Vertrag und Tests gemeinsam. Entfernen oder verschieben Sie einen redundanten Test erst, wenn klar ist, welcher Test das Risiko weiter schützt. Ein sporadisch fehlschlagender Test braucht einen Verantwortlichen und einen Reparaturtermin; dauerhaftes Deaktivieren versteckt nur die Lücke.
Eine Veröffentlichungsfunktion als konkreter Plan
Für den hypothetischen Editor genügt zunächst ein überschaubarer Plan:
- Policy: Unit-Fälle für fehlenden Inhalt, Leerraum und veröffentlichbaren Inhalt. HTTP-Setup ist unnötig.
- Daten und Zugriff: Integrationstest für Artikel- und Übersetzungsauswahl; API-Fälle für Ablehnung ohne Änderung und erlaubte Veröffentlichung mit gespeichertem Ergebnis.
- Editor: Komponententests für laufendes Speichern, Validierung, erhaltene Eingaben nach Fehlern und Erfolg. Ein Adaptertest prüft die dafür verwendete Antwort- und Fehlerabbildung.
- Gesamtablauf: Ein Browsertest meldet sich an, bearbeitet und veröffentlicht. Ein frischer Lesezugriff bestätigt das tatsächliche Ergebnis. Prüfen Sie vollständiges Neuladen, wenn Serverrendering relevant ist; ergänzen Sie einen Wiederherstellungsablauf nur für ein eigenständiges Risiko.
- Release: Ein sicherer Smoke-Test prüft die bereitgestellte Route. Cache- oder Abfragebudgets kommen hinzu, wenn die Funktion dieses Risiko mitbringt.
Beginnen Sie in einer gewachsenen Anwendung mit dem Verhalten, das Sie ändern wollen. Sichern Sie nötigenfalls zunächst einen ungeschützten kritischen Ablauf. Sobald Sie den Code besser kennen, können detaillierte Varianten näher an der Regel geprüft werden. Dafür muss weder die ganze Suite umgeschrieben noch ein beliebiges Coverage-Ziel erreicht werden.
Der nächste Test sollte eine benannte Unsicherheit klären. Platzieren Sie ihn dort, wo er das zuverlässig kann, und bewerten Sie den Nutzen daran, wie schnell sich Änderungen umsetzen und Fehler erklären lassen.
Technische Quellen
- Tests in Symfony 7.4: https://symfony.com/doc/7.4/testing.html
- Benutzerinteraktionen mit Testing Library: https://testing-library.com/docs/user-event/intro/
- Next.js mit Vitest und aktuelle Einschränkungen: https://nextjs.org/docs/app/guides/testing/vitest
- TypeScript-Typzusicherungen und Laufzeitgrenzen: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
