Cypress Netzwerk-Stubbing mit cy.intercept()
PASS
expect()
Testing · Cypress · E2E-Testing · Magento 2
Cypress Netzwerk-Stubbing mit cy.intercept()
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.

14 Min. Lesezeit cy.intercept() · Fixtures · GraphQL Magento 2.4.8 · Hyvä Theme · Cypress 13

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.

11. FAQ: Cypress Netzwerk-Stubbing mit cy.intercept()

1Was ist cy.intercept() und wofür wird es in Cypress genutzt?
Die Cypress-API zum Abfangen, Beobachten und Stubben von Netzwerk-Requests im Browser. Ersetzt echte Backend-Antworten durch kontrollierte, reproduzierbare Antworten für schnellere, deterministische E2E-Tests.
2Wie unterscheidet sich Stubbing von Spying bei cy.intercept()?
Spying beobachtet den echten Request ohne Änderung, Stubbing ersetzt die Antwort per req.reply() komplett. Kombinierbar über req.continue() und req.reply() im selben Handler.
3Wie lade ich eine Fixture-Datei in einen Intercept?
Inline mit { fixture: 'products/list.json' } oder explizit über cy.fixture(...).then(), wenn die Daten vorher modifiziert werden müssen.
4Wie warte ich zuverlässig auf einen abgefangenen Request?
Mit cy.wait('@alias'). Cypress wartet exakt bis zum Requestabschluss, statt eine feste Zeitspanne mit cy.wait(ms) verstreichen zu lassen.
5Wie simuliere ich einen 500 Fehler oder eine langsame Antwort?
Fehler über { statusCode: 500, body: {...} }, langsame Antworten über req.reply({ delay: 3000, fixture: '...' }) im Handler.
6Wie teste ich einen leeren Ergebniszustand (Empty State)?
Über eine Fixture mit leerem items-Array, etwa empty.json. So lässt sich prüfen, ob die UI eine passende Leer-Meldung statt eines Fehlers zeigt.
7Wie prüfe ich den Request-Body eines abgefangenen POST-Requests?
Direkt im Handler über req.body vor req.reply(), oder nachträglich über das interception-Objekt von cy.wait('@alias') mit .its('request.body').
8Wie intercepte ich Magento-GraphQL-Requests gezielt nach Operation?
Im Handler req.body.operationName auswerten und req.alias dynamisch setzen, da alle Operationen über denselben /graphql-Endpunkt laufen.
9Wann sollte ich NICHT stubben, sondern gegen die echte API testen?
Bei Checkout und Zahlungsabwicklung, da zu starkes Stubbing dort False Confidence erzeugt. Zusätzlich in einer Nightly-Suite gegen echtes Staging testen, ergänzt um Contract-Tests.
10Was ist der Unterschied zwischen Glob-Pattern und RegExp beim URL-Matching?
Glob-Pattern sind einfach und meist ausreichend, RegExp erlaubt präziseres Matching, etwa um Überschneidungen mit ähnlichen Endpunkten gezielt zu vermeiden.