04 / Blog · 09.09.2026
Der Dry Run muss scheitern können
Eine Simulation ist nur dann wertvoll, wenn sie dieselben Verträge, Zustände und Fehlerpfade wie die echte Integration abbildet. Für TYPO3-Projekte entsteht daraus eine klare Architektur: fachlicher Kern, austauschbare Adapter, kontrollierte Live-Tests und sichtbare Freigaben.
Das gefährliche Grün
Externe Integrationen lassen sich erstaunlich leicht scheinbar fertigstellen. Der Versanddienst nimmt laut Testmodus jede E-Mail an, der DNS-Adapter liefert eine erfundene Zone zurück und die simulierte Zahlungsanfrage endet zuverlässig mit success. Die Oberfläche wird grün, die Pipeline läuft durch – und trotzdem ist kaum etwas bewiesen.
Ein solcher Dry Run prüft oft nur, ob der eigene Code den gewünschten Idealfall durchlaufen kann. Er beantwortet nicht, wie das System auf ein abgelaufenes Token, eine gedrosselte API, eine verspätete Antwort oder ein bereits vorhandenes externes Objekt reagiert. Besonders problematisch wird es, wenn Simulation und Live-Betrieb getrennte Implementierungen mit unterschiedlichem Verhalten besitzen.
Die bessere Grundannahme lautet: Eine Simulation ist kein bequemer Ersatz für die Außenwelt. Sie ist eine kontrollierbare Implementierung desselben fachlichen Vertrags.
Das verändert die Architektur. Nicht ein globaler Schalter dryRun=true sollte beliebige Seiteneffekte unterdrücken. Stattdessen braucht jede Integrationsgrenze eine explizite Schnittstelle, definierte Ergebnisse und bekannte Fehler. Erst dann lassen sich Simulation, automatisierter Test und echter Anbieter sinnvoll aufeinander beziehen.
Zuerst den Vertrag modellieren
Anwendungslogik sollte nicht wissen müssen, mit welchem SDK ein Anbieter angesprochen wird oder welche URL gerade gilt. Sie sollte eine fachliche Fähigkeit verwenden. Für einen Deployment-Prozess könnte der Vertrag beispielsweise so aussehen:
interface DnsProvisioner
{
public function createRecord(
Hostname $hostname,
IpAddress $target,
IdempotencyKey $key,
): ProvisioningResult;
public function inspectRecord(Hostname $hostname): RecordState;
public function removeRecord(
Hostname $hostname,
IdempotencyKey $key,
): RemovalResult;
}
Die Typen sind hier wichtiger als die konkrete Syntax. ProvisioningResult sollte nicht nur true oder false enthalten, sondern etwa zwischen created, already_present, pending und rejected unterscheiden. Ein Fehler benötigt ebenfalls Bedeutung: Nicht autorisiert, Rate Limit, ungültiger Hostname und vorübergehende Nichterreichbarkeit verlangen unterschiedliche Reaktionen.
TYPO3 unterstützt diese Trennung mit seinem auf Symfony basierenden Dependency-Injection-System; die aktuelle Core-Dokumentation empfiehlt Constructor Injection für Dienstabhängigkeiten. Interfaces können dadurch je nach Umgebung auf verschiedene Implementierungen zeigen. Das ist kein spezieller Testtrick, sondern reguläre Anwendungsarchitektur. (TYPO3 Dependency Injection)
Ein fachlicher Vertrag schützt außerdem vor einem verbreiteten Leck: Gibt ein Service direkt die Response-Klasse eines Anbieter-SDKs zurück, breitet sich dessen Datenmodell im eigenen Code aus. Ein späterer Wechsel betrifft dann Controller, Jobs, Templates und Tests. Ein eigener, kleiner Ergebnistyp hält die Übersetzung dort, wo sie hingehört – im Adapter.
Drei Ebenen, drei verschiedene Beweise
„Die Integration ist getestet“ ist zu ungenau. Mindestens drei Ebenen beantworten unterschiedliche Fragen.
1. Deterministische Simulation
Die Simulation läuft ohne Netzwerk, Zugangsdaten oder echte Infrastruktur. Sie prüft den fachlichen Prozess: Werden Datensätze im richtigen Zustand angelegt? Wird eine Einladung erst nach erfolgreichem Deployment freigegeben? Bleibt ein fehlgeschlagener Schritt wiederholbar? Wird eine Bereinigung nur für Ressourcen geplant, die dem System tatsächlich gehören?
Dafür sollte die simulierte Implementierung zustandsbehaftet sein. Ein DNS-Eintrag, der im ersten Schritt erzeugt wurde, muss beim nächsten Lesezugriff vorhanden sein. Ein zweiter identischer Erzeugungsversuch sollte already_present liefern – oder gezielt einen Konflikt, falls dies dem echten Vertrag entspricht.
Eine gute Simulation kann Fehler injizieren:
$dns = SimulatedDnsProvisioner::withScenario(
CreateRecordScenario::rateLimited(retryAfter: 30),
);
So lassen sich nicht nur glückliche Pfade, sondern auch Wiederholungen, Abbruch und Bedienoberfläche reproduzierbar prüfen. Uhr, Zufallsgenerator und ID-Erzeugung gehören dabei ebenfalls hinter kontrollierbare Abhängigkeiten. Sonst bleibt ein vermeintlich deterministischer Test von realer Zeit oder zufälligen Werten abhängig.
2. Adapter- und Vertragstests
Diese Ebene prüft die Übersetzung zwischen eigenem Vertrag und Anbieterprotokoll. Sendet der Adapter die richtige HTTP-Methode, Authentifizierung und Nutzlast? Werden 429, 401 und fehlerhaftes JSON korrekt klassifiziert? Bleiben optionale Felder tatsächlich optional?
Der Symfony HTTP Client stellt dafür MockHttpClient und MockResponse bereit. Weil der Mock dasselbe HttpClientInterface implementiert wie der echte Client, kann ein Test Requests untersuchen und kontrollierte Antworten, Statuscodes oder Fehler liefern, ohne das Netzwerk aufzurufen. Die Dokumentation hebt ausdrücklich die schnellere, konsistente Ausführung und das Vermeiden unbeabsichtigter Zustandsänderungen hervor. (Symfony HTTP Client: Testing)
Das passt zur TYPO3-Teststrategie: Unit-Tests isolieren einzelne Klassen und mocken unter anderem API-Aufrufe; Functional Tests laufen dagegen in einer vollständig aufgebauten TYPO3-Instanz mit Datenbank und Extension-Konfiguration. Beide Ebenen sind nötig, weil ein korrekter Adapter allein noch nicht beweist, dass Service-Konfiguration, Persistenz und Rechteprüfung zusammenspielen. (TYPO3 Extension Testing)
Adaptertests sollten auch die ausgehende Anfrage prüfen. Ein fest einprogrammiertes Erfolgsobjekt testet nur die Verarbeitung einer Antwort, nicht den Vertrag zum Anbieter. Ebenso sinnvoll sind gespeicherte, bereinigte Antwort-Fixtures – solange sie keine Zugangsdaten oder personenbezogenen Inhalte enthalten und ihre Herkunft sowie API-Version dokumentiert bleiben.
3. Begrenzter Live-Smoke-Test
Kein Mock beweist, dass aktuelle Zugangsdaten gelten, DNS und TLS funktionieren oder der Anbieter seine Dokumentation genauso umsetzt wie erwartet. Dafür braucht es einen kleinen Live-Test gegen eine Sandbox oder eine eindeutig wegwerfbare Ressource.
Dieser Test sollte jedoch nicht Teil jedes lokalen Testlaufs sein. Er benötigt:
- eine explizite Aktivierung,
- eng begrenzte Zugangsdaten,
- eindeutig benannte Testressourcen,
- eine bekannte maximale Reichweite,
- verlässliche Bereinigung,
- protokollierte externe IDs,
- ein Laufzeit- und Kostenlimit.
Der Live-Smoke-Test beweist die Verbindung zur Außenwelt. Er ersetzt weder die breite Simulation noch die präzisen Adaptertests. Seltene Fehler wie Rate Limits oder verzögerte Konsistenz lassen sich live kaum zuverlässig erzwingen und gehören deshalb weiterhin in kontrollierte Szenarien.
Ein Null-Adapter darf keinen Erfolg erfinden
Optionale Integrationen führen häufig zu einer Null-Implementierung. Das kann sinnvoll sein: Eine Anwendung bleibt lokal nutzbar, obwohl Social Publishing, Kartenabrechnung oder KI-Beratung nicht eingerichtet sind.
Gefährlich wird es, wenn der Null-Adapter einfach Erfolg meldet. Dann sieht die Fachlogik keinen Unterschied zwischen „extern veröffentlicht“ und „bewusst nichts getan“. Statusanzeigen, Audit-Daten und Folgeprozesse erzählen eine falsche Geschichte.
Ein Null-Adapter sollte stattdessen ein eindeutiges Ergebnis wie disabled liefern. Ist eine Funktion in der Konfiguration aktiviert, aber der echte Adapter oder seine Zugangsdaten fehlen, muss die Anwendung geschlossen fehlschlagen. Drei Zustände sollten nicht vermischt werden:
disabled → Funktion ist bewusst nicht aktiv
simulated → Ablauf wurde ohne externen Seiteneffekt geprüft
live → echter Adapter wurde kontrolliert ausgeführt
Diese Information gehört in Ergebnis und Protokoll, nicht nur in eine flüchtige Umgebungsvariable. Auch später muss nachvollziehbar bleiben, ob eine E-Mail tatsächlich übergeben oder lediglich simuliert wurde.
Simulation ist eine eigene Laufzeitumgebung
Ein einzelner globaler Dry-Run-Schalter wird bei längeren Prozessen schnell unbrauchbar. Vielleicht sollen Lighthouse-Audits echt laufen, DNS und Deployment aber simuliert bleiben. Oder E-Mails sollen technisch gerendert, jedoch an einen lokalen Posteingang umgeleitet werden. Die Betriebsart sollte deshalb pro Fähigkeit konfiguriert und vor dem Start validiert werden.
Eine mögliche Konfiguration lautet:
integrations:
audit: live
dns: simulated
deployment: simulated
mail: redirected
Die Anwendung sollte daraus beim Booten oder spätestens vor dem Jobstart eine effektive Konfiguration ableiten. Ein aktivierter Live-Modus ohne Credentials ist ein Konfigurationsfehler. Ein produktives System mit versehentlich simuliertem Versand braucht mindestens eine unübersehbare Warnung – bei kritischen Abläufen besser eine Startblockade.
Sensible Werte gehören nicht in Simulationsprotokolle. Gute Testbarkeit entsteht durch kleine Verträge und kontrollierbare Antworten, nicht durch das Kopieren produktiver Secrets oder vollständiger Produktionsdaten in eine lokale Umgebung.
Zustände müssen auch nach einem Absturz stimmen
Externe Seiteneffekte und lokale Datenbanktransaktionen lassen sich meist nicht atomar zusammenfassen. Ein DNS-Anbieter kann einen Eintrag erfolgreich erzeugen, während der anschließende lokale Schreibvorgang fehlschlägt. Beim Wiederanlauf weiß das System dann zunächst nicht, ob es erneut erzeugen oder nur abgleichen soll.
Deshalb braucht eine robuste Integration mehr als einen Adapter:
- Vor dem Aufruf wird die Absicht mit einer stabilen Idempotenz-ID gespeichert.
- Der Adapter übermittelt diese ID, sofern der Anbieter das unterstützt.
- Externe Objekt-IDs und bereinigte Resultate werden persistiert.
- Nach unklarem Ausgang folgt zunächst eine Zustandsabfrage statt blinder Wiederholung.
- Bereinigung arbeitet nur auf eindeutig zugeordneten Ressourcen.
Die Simulation muss genau diese Zwischenlagen darstellen können. Andernfalls bleibt der schwierigste Teil des Prozesses ungetestet.
Auch Beobachtbarkeit sollte denselben Vertrag abbilden. Korrelation über Prozess- und Netzwerkgrenzen ist ein Kernprinzip verteilter Traces; OpenTelemetry beschreibt Context Propagation als Mechanismus, mit dem Signale über mehrere Dienste hinweg zusammengeführt werden können. Für eine Integration heißt das praktisch: Job-ID, Korrelations-ID und – soweit sicher möglich – externe Request-ID sollten zusammen auffindbar sein. (OpenTelemetry Context Propagation)
Zwei Projekte mit bewusst kontrollierten Nähten
In den bereitgestellten Projektinformationen zu TYPO3 Demo besitzt jeder externe Schritt eine deterministische Simulation: Scraping, Audit, Generierung, DNS, Deployment, Validierung, E-Mail und Lebenszyklus. Live-DNS, Deployment und Versand bleiben hinter einer expliziten Betreiberfreigabe. Eine Einladung ist zudem an den laufenden Zustand der erzeugten Instanz gebunden. Das verhindert laut Projektbeschreibung, dass alte oder bereits gelöschte Demo-Ziele weitergegeben werden.
Bei Softwair sind externe Anbieter über Interfaces isoliert. Social Publishing, Karten-Zahlungen und KI-Beratung besitzen standardmäßig Null-Implementierungen. Ein aktivierter Schalter ohne geprüften Adapter und Zugangsdaten darf dort keinen Erfolg vortäuschen. Das Projekt ist als Fundament umgesetzt, aber nicht produktiv betrieben; Domains, SMTP, Backups und verantwortliche Konten bleiben ausdrücklich offene Betreiberentscheidungen.
Das sind konkrete Eigenschaften der bereitgestellten Projekte, keine Aussage darüber, dass sämtliche beschriebenen Teststufen bereits für jeden denkbaren Anbieter live durchlaufen wurden. Die allgemeine Lehre daraus ist breiter: Je größer die mögliche Außenwirkung, desto klarer müssen simulierte, deaktivierte und echte Ausführung voneinander getrennt sein.
Eine praktische Abnahmeliste
Vor der Freigabe einer externen Integration sollten sich einige Fragen eindeutig beantworten lassen:
- Existiert ein fachlicher Vertrag ohne Anbieterklassen im Rückgabewert?
- Sind Erfolg, Deaktivierung, Simulation, temporärer Fehler und endgültige Ablehnung unterscheidbar?
- Kann die Simulation Zustand halten und gezielte Fehler erzeugen?
- Prüfen Adaptertests sowohl ausgehende Requests als auch eingehende Antworten?
- Läuft der Prozess als Functional Test in einer echten TYPO3-Instanz?
- Gibt es einen begrenzten Live-Smoke-Test mit eigener Bereinigung?
- Sind Wiederholungen idempotent oder durch vorherige Zustandsabfrage abgesichert?
- Bleiben Modus, Korrelations-ID und externe Objekt-ID nachvollziehbar?
- Schlägt eine unvollständige Live-Konfiguration sichtbar fehl?
- Kann die Anwendung beweisen, dass eine Ressource ihr gehört, bevor sie diese verändert oder löscht?
Nicht jede kleine API benötigt dieselbe Tiefe. Ein lesender Wetterdienst verlangt andere Schutzmaßnahmen als DNS, Zahlung oder automatischer Versand. Die Struktur bleibt jedoch ähnlich: Je irreversibler, teurer oder öffentlicher ein Seiteneffekt ist, desto enger muss sein Live-Vertrag sein.
Der Dry Run ist ein Gegenentwurf zur Hoffnung
Eine Simulation schafft keine Sicherheit, wenn sie lediglich überall Erfolg zurückgibt. Ihr Wert entsteht aus kontrollierter Unbequemlichkeit: Sie muss Fehler reproduzieren, Zustände behalten und dieselben fachlichen Ergebnisse liefern wie der echte Adapter.
Damit wird der Dry Run zu einem ernsthaften Teil der Anwendung. Er erlaubt breite Tests ohne Außenwirkung, während Adaptertests die Protokollübersetzung und wenige Live-Smokes die reale Verbindung prüfen. Keine dieser Ebenen ersetzt die andere.
Der entscheidende Qualitätsmaßstab lautet deshalb nicht: „Kann der Testmodus die Pipeline grün machen?“ Sondern: „Kann er präzise zeigen, warum diese Pipeline im echten Betrieb nicht grün werden dürfte?“