Deterministische E2E-Tests statt Flaky Tests
Wer Magento-Checkout-Flows nur gegen die echte API testet, bekommt langsame und flackernde End-to-End-Tests, die bei jedem Backend-Hickup rot werden. Mit cy.intercept() stubbt und beobachtet ihr Netzwerk-Requests gezielt, ladet Fixtures für realistische Antworten und testet Fehler- und Ladezustände deterministisch, ganz ohne den echten Server dafür anzufassen. So bleiben eure Tests schnell, reproduzierbar und unabhängig vom aktuellen Datenbankzustand.
Inhaltsverzeichnis
- 1. Warum Netzwerk-Stubbing beim E2E-Testing von Magento-Shops entscheidend ist
- 2. cy.intercept() Grundlagen: Syntax, Matching und Aliasing
- 3. Fixture-basiertes Response-Mocking mit cypress/fixtures/*.json
- 4. Deterministisches Warten: cy.wait('@alias') statt fester Timeouts
- 5. Fehler- und Ladezustände deterministisch testen
- 6. Request- und Response-Payloads assertieren
- 7. req.continue() und req.reply(): Spying vs. Stubbing im Detail
- 8. Magento-GraphQL-Requests nach Operation-Name aliasen
- 9. Wann stubben, wann echten Backend nutzen: Contract-Test-Tradeoffs
- 10. Zusammenfassung
- 11. FAQ
1. Warum Netzwerk-Stubbing beim E2E-Testing von Magento-Shops entscheidend ist
End-to-End-Tests, die ausschließlich gegen einen echten Magento-Backend laufen, sind strukturell instabil: Produktbestände ändern sich, Preise werden neu kalkuliert, Drittanbieter-APIs für Zahlungen oder Versand antworten unterschiedlich schnell, und jede Testumgebung driftet über die Zeit auseinander. Ein Test, der heute grün ist, kann morgen allein deshalb rot werden, weil ein Produkt aus dem Katalog verschwunden ist. cy.intercept() löst dieses Problem, indem es Netzwerk-Requests auf Ebene des Browsers abfängt, bevor sie das Backend erreichen, und stattdessen kontrollierte, reproduzierbare Antworten zurückgibt.
Das ist bewusst von reinem PHPUnit-Unit-Testing abzugrenzen: Während Unit-Tests einzelne PHP-Klassen isoliert prüfen, testet Cypress das komplette System aus Sicht des Browsers, inklusive Alpine.js-Interaktionen, GraphQL-Aufrufen und der tatsächlichen Rendering-Logik im Hyvä-Theme. Netzwerk-Stubbing ist dabei kein Ersatz für Backend-Tests, sondern eine gezielte Kontrolle über genau die Variable, die E2E-Tests am häufigsten instabil macht: die Antwort des Servers. Wer diese Variable beherrscht, kann Edge Cases wie leere Warenkörbe, fehlgeschlagene Zahlungen oder ausverkaufte Produkte reproduzierbar erzwingen, ohne die Testdatenbank manuell präparieren zu müssen.
2. cy.intercept() Grundlagen: Syntax, Matching und Aliasing
cy.intercept() gibt es in mehreren Signaturvarianten: cy.intercept(url) für reines Beobachten, cy.intercept(method, url, response) für einfaches Stubbing und cy.intercept(routeMatcher, handler) für die volle Kontrolle über Request und Response per Callback-Funktion. Für das URL-Matching stehen zwei Ansätze zur Verfügung: Glob-Pattern wie '**/rest/V1/products*' sind schnell geschrieben und für die meisten REST-Endpunkte ausreichend, während RegExp-Objekte wie /\/rest\/V1\/products\/\d+$/ präzises Matching erlauben, wenn sich mehrere Endpunkte sonst überschneiden würden.
Jeder Intercept sollte über .as('alias') einen sprechenden Alias erhalten. Ohne Alias lässt sich ein abgefangener Request später weder gezielt abwarten noch in Assertions referenzieren. Wichtig ist außerdem die Reihenfolge: Cypress wertet mehrere passende cy.intercept()-Definitionen in umgekehrter Registrierungsreihenfolge aus, das zuletzt registrierte Pattern gewinnt bei Überschneidungen. Das erlaubt es, in einem Test einen globalen Default-Stub zu setzen und ihn in einzelnen it()-Blöcken gezielt zu überschreiben, etwa für einen einzelnen Fehlerfall.
// Basic GET intercept with fixture-based response mocking
cy.intercept('GET', '/rest/V1/products*', {
fixture: 'products/list.json'
}).as('getProducts');
cy.visit('/catalogsearch/result/?q=hyva');
cy.wait('@getProducts');
// Glob pattern matches any query string after products
cy.intercept('GET', '**/rest/V1/products**').as('getProductsGlob');
// RegExp pattern for precise path matching, no accidental overlap
cy.intercept({
method: 'GET',
url: /\/rest\/V1\/products\/\d+$/
}).as('getSingleProduct');
3. Fixture-basiertes Response-Mocking mit cypress/fixtures/*.json
Fixtures sind statische JSON-Dateien unter cypress/fixtures/, die realistische Backend-Antworten simulieren. Statt eine Response inline im Test zu definieren, referenziert man eine Datei über { fixture: 'products/list.json' } oder lädt sie explizit per cy.fixture('products/list.json').then((data) => ...), wenn die Daten vor der Verwendung noch modifiziert werden müssen. Der Vorteil gegenüber Inline-Objekten: Fixtures sind versioniert, wiederverwendbar über mehrere Spec-Dateien hinweg und lassen sich klar benennen, etwa empty.json, out-of-stock.json oder 500-error.json für die jeweiligen Edge Cases.
Eine gute Fixture-Struktur orientiert sich am tatsächlichen Shape der Magento-REST- oder GraphQL-Antwort, inklusive aller Felder, auf die das Frontend zugreift. Unvollständige Fixtures sind eine häufige Fehlerquelle: Fehlt ein Feld wie extension_attributes, das die Komponente eigentlich erwartet, kann der Test fälschlich grün bleiben, obwohl das reale Backend ein anderes Shape liefert. Deshalb lohnt es sich, Fixtures regelmäßig gegen eine echte API-Antwort abzugleichen oder sie automatisiert aus einem Staging-System zu generieren, statt sie einmalig von Hand zu schreiben und nie wieder zu aktualisieren.
{
"items": [
{
"id": 1042,
"sku": "MS-HOODIE-BLK-M",
"name": "Mironsoft Hoodie Black",
"price": 59.9,
"status": 1,
"extension_attributes": {
"stock_item": {
"qty": 24,
"is_in_stock": true
}
}
},
{
"id": 1043,
"sku": "MS-CAP-GRN",
"name": "Mironsoft Cap Green",
"price": 24.5,
"status": 1,
"extension_attributes": {
"stock_item": {
"qty": 0,
"is_in_stock": false
}
}
}
],
"total_count": 2
}
4. Deterministisches Warten: cy.wait('@alias') statt fester Timeouts
Ein klassisches Anti-Pattern in E2E-Tests ist cy.wait(2000): eine feste Wartezeit, die entweder zu kurz ist und den Test flaky macht, oder zu lang und die gesamte Test-Suite unnötig verlangsamt. cy.wait('@alias') löst dieses Problem strukturell, indem es exakt so lange wartet, bis der aliasierte Request tatsächlich abgeschlossen ist, egal ob das 50 Millisekunden oder 3 Sekunden dauert. Erst danach führt Cypress den nächsten Befehl aus, wodurch Race Conditions zwischen UI-Update und Netzwerk-Antwort ausgeschlossen sind.
cy.wait() akzeptiert auch ein Array von Aliassen, etwa cy.wait(['@getProducts', '@getCategories']), wenn eine Seite mehrere parallele Requests auslöst und der Test erst fortfahren soll, sobald alle abgeschlossen sind. Zusätzlich liefert cy.wait('@alias') ein interception-Objekt zurück, das sich direkt für Assertions weiterverwenden lässt, etwa .its('response.statusCode').should('eq', 200). Wichtig: Ein Timeout bei cy.wait() ist selbst ein wertvolles Testergebnis, es zeigt zuverlässig an, dass ein erwarteter Request nie ausgelöst wurde, was auf einen echten Bug im Frontend hindeuten kann.
5. Fehler- und Ladezustände deterministisch testen
Fehlerzustände wie ein 500er vom Server oder ein Timeout lassen sich mit einem echten Backend kaum reproduzierbar erzeugen, mit cy.intercept() dagegen mit einer einzigen Zeile. Über { statusCode: 500, body: {...} } simuliert man einen Backend-Fehler exakt dann, wenn der Test ihn braucht, und prüft anschließend, ob die UI die erwartete Fehlermeldung anzeigt statt einfach hängen zu bleiben. Für einen kompletten Netzwerkausfall eignet sich forceNetworkError: true, das simuliert einen abgebrochenen Request statt einer regulären Fehlerantwort.
Für Ladezustände ist die delay-Option im req.reply()-Handler entscheidend: Sie verzögert die Antwort künstlich um beispielsweise 3 Sekunden, sodass ein Test gezielt prüfen kann, ob während dieser Zeit ein Loading-Spinner sichtbar ist und danach korrekt wieder verschwindet. Ohne diese künstliche Verzögerung sind Loading-States in der Praxis kaum zuverlässig zu testen, weil lokale Testumgebungen meist zu schnell antworten, um den Zwischenzustand überhaupt sichtbar zu machen.
// Simulate a hard backend failure
cy.intercept('GET', '**/rest/V1/products*', {
statusCode: 500,
body: { message: 'Internal Server Error' }
}).as('getProductsError');
cy.visit('/catalog/category/view/id/5');
cy.wait('@getProductsError');
cy.get('[data-testid="error-banner"]').should('be.visible');
// Simulate a slow response to test loading spinners deterministically
cy.intercept('GET', '**/rest/V1/products*', (req) => {
req.reply({ delay: 3000, fixture: 'products/list.json' });
}).as('getProductsSlow');
cy.get('[data-testid="loading-spinner"]').should('be.visible');
cy.wait('@getProductsSlow');
cy.get('[data-testid="loading-spinner"]').should('not.exist');
6. Request- und Response-Payloads assertieren
Netzwerk-Stubbing dient nicht nur dazu, Antworten vorzugeben, sondern auch dazu, zu prüfen, was das Frontend tatsächlich an das Backend sendet. Im Callback-Handler von cy.intercept() steht der eingehende req vollständig zur Verfügung, inklusive req.body, req.headers und req.url. So lässt sich direkt im Handler assertieren, etwa dass ein Checkout-Request die korrekte Zahlungsmethode enthält, bevor überhaupt eine Antwort zurückgegeben wird: expect(req.body.paymentMethod.method).to.eq('checkmo').
Alternativ liefert das von cy.wait('@alias') zurückgegebene interception-Objekt denselben Request nachträglich zur Prüfung, etwa über cy.wait('@placeOrder').its('request.body.cartId').should('exist'). Diese nachträgliche Variante eignet sich besonders gut, wenn mehrere Assertions nacheinander lesbar in der Testkette stehen sollen. Für komplexere Prüfungen, etwa ob ein Warenkorb-Request exakt die erwarteten Line-Items in der richtigen Reihenfolge enthält, empfiehlt sich eine Kombination aus deep.equal und gezielten Property-Checks, um Fehlermeldungen bei einem fehlgeschlagenen Test aussagekräftig zu halten.
// Intercept POST checkout request and assert on the outgoing payload
cy.intercept('POST', '**/rest/V1/carts/*/payment-information', (req) => {
expect(req.body).to.have.property('paymentMethod');
expect(req.body.paymentMethod.method).to.eq('checkmo');
req.reply({ statusCode: 200, body: 12345 });
}).as('placeOrder');
cy.get('[data-testid="place-order"]').click();
cy.wait('@placeOrder').its('response.statusCode').should('eq', 200);
7. req.continue() und req.reply(): Spying vs. Stubbing im Detail
Ein cy.intercept() ohne definierte Response arbeitet als reiner Spy: Der Request geht unverändert an das echte Backend, Cypress beobachtet ihn nur und macht ihn über cy.wait('@alias') und das interception-Objekt prüfbar. Das ist nützlich, um zu verifizieren, dass ein Tracking-Event oder ein Analytics-Call tatsächlich ausgelöst wird, ohne dessen Verhalten zu verändern. Innerhalb eines Handlers lässt sich dasselbe Verhalten explizit mit req.continue() erzwingen, optional mit einer Callback-Funktion, die die Response nach Erhalt noch inspiziert oder modifiziert.
req.reply() dagegen stubbt die Antwort vollständig und verhindert, dass der Request das echte Backend überhaupt erreicht. Beide Funktionen lassen sich kombinieren: req.continue((res) => { res.body.items = res.body.items.slice(0, 1); }) lässt den Request real ablaufen, verändert aber die zurückkommende Antwort, etwa um eine große Produktliste im Test künstlich zu verkürzen. Diese Zwischenform ist besonders wertvoll für Snapshot-artige Tests, bei denen die reale Backend-Logik weiterhin greifen soll, die Datenmenge im Test aber kontrollierbar bleiben muss.
8. Magento-GraphQL-Requests nach Operation-Name aliasen
Magentos GraphQL-API läuft über einen einzigen Endpunkt, meist /graphql, wodurch reines URL-Matching für cy.intercept() nutzlos wird: Jede Query und jede Mutation landet auf derselben Route. Die Lösung liegt im Request-Body: Jede GraphQL-Anfrage enthält ein operationName-Feld, das sich im Callback-Handler auswerten lässt, um gezielt einzelne Operationen zu aliasen. So kann ein Test präzise auf ProductDetailQuery warten, ohne versehentlich auf eine völlig andere Query wie MegaMenu zu reagieren, die zufällig zur gleichen Zeit feuert.
Das dynamische Setzen von req.alias im Handler ist dabei der zuverlässigste Ansatz, weil er auch funktioniert, wenn mehrere GraphQL-Operationen innerhalb kurzer Zeit an dieselbe URL gesendet werden. Ohne dieses gezielte Aliasing bleibt nur ein generischer cy.intercept('POST', '**/graphql')-Fang, der zwar alle Requests abfängt, aber keine Unterscheidung zwischen einzelnen Operationen erlaubt und Tests damit unpräzise und fehleranfällig macht, insbesondere bei Seiten mit vielen parallelen GraphQL-Aufrufen wie der Magento-Produktdetailseite.
// Alias Magento GraphQL requests by operation name, not by URL
cy.intercept('POST', '**/graphql', (req) => {
if (req.body.operationName === 'ProductDetail') {
req.alias = 'productDetailQuery';
}
if (req.body.operationName === 'AddSimpleProductsToCart') {
req.alias = 'addToCartMutation';
}
});
cy.visit('/simple-product.html');
cy.wait('@productDetailQuery');
cy.get('[data-testid="add-to-cart"]').click();
cy.wait('@addToCartMutation').its('response.body.data').should('exist');
9. Wann stubben, wann echten Backend nutzen: Contract-Test-Tradeoffs
Netzwerk-Stubbing macht Tests schnell und deterministisch, birgt aber ein reales Risiko: False Confidence. Wenn eine Fixture nicht mehr zum tatsächlichen API-Shape passt, weil sich das Backend geändert hat, bleibt der Cypress-Test trotzdem grün, obwohl die Integration in Produktion längst gebrochen ist. Deshalb gilt die Faustregel, dass kritische Pfade wie der komplette Checkout-Abschluss oder die Zahlungsabwicklung mindestens in einer Nightly-Suite gegen ein echtes Staging-Backend laufen sollten, während UI-Zustände wie Fehler, leere Listen oder Ladeanimationen gezielt gestubbt werden.
Eine sinnvolle Ergänzung sind dedizierte Contract-Tests, etwa mit Pact oder einer OpenAPI-Schema-Validierung, die separat von den UI-Tests sicherstellen, dass das reale Backend weiterhin dem Shape entspricht, den die Fixtures annehmen. So bleibt die Verantwortung sauber getrennt: Cypress mit cy.intercept() prüft das Frontend-Verhalten bei gegebener Antwort, Contract-Tests prüfen, ob diese Antwort auch der Realität entspricht. Die folgende Tabelle fasst zusammen, wann welches Pattern die bessere Wahl ist.
| Szenario | Naives Pattern | Risiko | Empfohlenes Pattern |
|---|---|---|---|
| Warten auf Requests | cy.wait(2000) mit fester Zeit | Flaky bei langsamem CI-Runner | cy.wait('@alias') auf Intercept |
| Checkout & Zahlung | Payment-Gateway komplett stubben | Vertragsbruch bleibt unbemerkt | Kritischen Pfad gegen Staging testen |
| Fehlerzustände | Gar nicht getestet | 500-Handling bricht unbemerkt in Prod | statusCode 500 gezielt stubben |
| GraphQL-Queries | Nur nach URL matchen | Alias-Kollisionen, falsche Interception | Matching über operationName |
| Leere Ergebnislisten | Nur Happy Path mit Daten | Empty State bleibt ungetestet | Fixture mit leerem Array stubben |
In der Praxis funktioniert eine hybride Strategie am besten: Der Großteil der Test-Suite läuft schnell und gestubbt gegen Fixtures, ein schlanker Satz an Smoke-Tests deckt die geschäftskritischen Pfade gegen ein echtes Backend ab, und Contract-Tests schließen die Lücke dazwischen ab. So bleibt die Suite schnell, ohne die Aussagekraft der Tests für die wichtigsten Nutzerflüsse zu opfern.
Mironsoft
Cypress-E2E-Testing und Testautomatisierung für Magento- und Hyvä-Shops
Stabile E2E-Tests für euren Magento-Shop?
Wir bauen belastbare Cypress-Test-Suites für eure Magento- und Hyvä-Shops, von der ersten Fixture-Strategie bis zur vollständigen CI/CD-Integration, damit eure kritischen Flows zuverlässig grün bleiben.
Cypress-Test-Setup
Intercepts, Fixtures und Alias-Strategien von Grund auf sauber aufgebaut
GraphQL-Testabdeckung
Operation-basiertes Aliasing für Magentos GraphQL-API
CI/CD-Integration
Cypress in eure Pipeline eingebunden, mit stabilen, schnellen Läufen
10. Zusammenfassung
cy.intercept() löst das Kernproblem instabiler E2E-Tests: die Abhängigkeit von einem echten, sich ständig verändernden Backend. Mit Fixture-basiertem Stubbing lassen sich Ladezustände, Fehlerfälle und Empty States reproduzierbar erzwingen, während cy.wait('@alias') Race Conditions zwischen UI und Netzwerk-Antwort strukturell ausschließt, statt sie mit festen Timeouts nur notdürftig zu kaschieren. Für Magentos GraphQL-API ist operation-basiertes Aliasing über den Request-Body der einzig zuverlässige Weg, einzelne Queries und Mutationen gezielt anzusprechen, da URL-Matching am einzigen /graphql-Endpunkt scheitert.
Der entscheidende Punkt liegt aber nicht im technischen Können, sondern in der Strategie: Stubbing ist mächtig, aber nur so vertrauenswürdig wie die Fixtures, auf denen es basiert. Kritische Geschäftsprozesse wie Checkout und Zahlung gehören zusätzlich in eine Suite, die gegen ein echtes Backend läuft, ergänzt um separate Contract-Tests, die den API-Shape absichern. So bleibt die Test-Suite schnell und gleichzeitig aussagekräftig, statt nur schnell und blind grün.
Cypress-Netzwerk-Stubbing mit cy.intercept() - Das Wichtigste auf einen Blick
cy.intercept() Grundlagen
Glob- oder RegExp-Matching, immer mit .as('alias') versehen, spätere Definitionen überschreiben frühere.
Fixtures
JSON-Dateien unter cypress/fixtures/, versioniert und wiederverwendbar für Edge Cases wie Empty States.
Deterministisches Warten
cy.wait('@alias') statt fester Timeouts, unterbindet Race Conditions strukturell.
Stub vs. echtes Backend
Kritische Pfade zusätzlich gegen Staging testen, Contract-Tests gegen API-Drift absichern.