Cypress Custom Commands: Wiederverwendbare Testbausteine
PASS
expect()
Cypress · Custom Commands · E2E-Testing · TypeScript
Cypress Custom Commands: Wiederverwendbare Testbausteine
Login, Warenkorb und TypeScript-Typing sauber kapseln

Wer Login und Warenkorb-Schritte in jeder Testdatei einzeln wiederholt, baut eine Cypress-Suite, die bei jeder UI-Änderung an dutzenden Stellen bricht. Custom Commands kapseln wiederkehrende Abläufe in wiederverwendbare, typsichere Bausteine und machen E2E-Tests für einen Magento-Shop lesbar, robust und wartbar.

14 Min. Lesezeit Cypress.Commands.add · cy.session · TypeScript Cypress 13.x · Magento 2.4.8 · Hyvä Theme

1. Warum Custom Commands in Cypress-Tests unverzichtbar sind

In jeder wachsenden Cypress-Suite taucht irgendwann derselbe Login-Ablauf, dieselbe Formularausfüllung oder derselbe Warenkorb-Klick in einem Dutzend Spec-Dateien identisch wieder auf. Das ist keine harmlose Redundanz, sondern eine wachsende Wartungslast: Ändert sich ein Selektor, ein Feldname oder die Reihenfolge der Schritte auf der Login-Seite, müssen alle Kopien synchron angepasst werden. In der Praxis passiert das selten vollständig, und die Suite beginnt an einzelnen Stellen unbemerkt zu lügen, weil ein Test noch den alten Ablauf nutzt.

Custom Commands lösen dieses Problem, indem sie einen Ablauf an genau einer Stelle definieren und über Cypress.Commands.add() im gesamten Projekt verfügbar machen. Der Test selbst liest sich dann als Abfolge fachlicher Schritte, cy.login(user), cy.addToCart(sku), statt als Liste von CSS-Selektoren und Klick-Koordinaten. Wichtig ist dabei, dass Custom Commands im Cypress Command Log weiterhin sichtbar aufgeklappt werden können, sodass Debugging nicht durch die Abstraktion erschwert wird, sondern nur die Wiederholung entfällt.

2. Cypress.Commands.add(): Parent-, Child- und Dual-Commands

Cypress unterscheidet bei der Registrierung über Cypress.Commands.add() drei Command-Typen, die über die Option prevSubject gesteuert werden. Ein Parent-Command wie cy.login() beginnt eine neue Kette und erwartet kein vorheriges Subject, es startet die Chain quasi neu, ähnlich wie cy.visit() oder cy.get(). Ein Child-Command wird mit { prevSubject: true } registriert und operiert auf dem Subject des vorherigen Befehls, etwa .getBySel('cart-badge'), das auf ein zuvor mit cy.get() ermitteltes Element aufsetzt.

Ein Dual-Command mit { prevSubject: 'optional' } funktioniert in beiden Rollen, je nachdem ob es am Kettenanfang oder nach einem anderen Befehl steht, was sich für generische Helfer wie cy.dataCy() eignet. Die Wahl des richtigen Typs ist keine Nebensächlichkeit: Ein als Parent registrierter Command, der eigentlich ein Element weiterverarbeiten sollte, bricht die Chain und macht spätere .should()-Assertions auf das falsche Subject unmöglich.


// cypress/support/commands.js

// Parent command: starts a new chain, no previous subject expected
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/customer/account/login');
  cy.get('#email').type(email);
  cy.get('#pass').type(password, { log: false });
  cy.get('#send2').click();
});

// Child command: operates on the subject yielded by the previous command
Cypress.Commands.add(
  'getBySel',
  { prevSubject: true },
  (subject, selector) => {
    return cy.wrap(subject).find(`[data-testid="${selector}"]`);
  }
);

// Dual command: works both as a chain starter and as a chained step
Cypress.Commands.add(
  'dataCy',
  { prevSubject: 'optional' },
  (subject, selector) => {
    const scope = subject ? cy.wrap(subject) : cy;
    return scope.find(`[data-cy="${selector}"]`);
  }
);

3. Login-Command mit cy.session bauen

Ein naiver Login-Command füllt bei jedem Testaufruf erneut das Formular aus, klickt den Submit-Button und wartet auf die Weiterleitung. Bei hunderten Tests, die Login voraussetzen, summiert sich das zu Minuten reiner Wartezeit pro Testlauf. cy.session() löst das, indem es den Session-Zustand, Cookies und Local Storage, nach dem ersten erfolgreichen Login cacht und bei jedem weiteren Aufruf mit identischer id sofort wiederherstellt, ohne die UI erneut zu durchlaufen.

Die setup-Funktion enthält den eigentlichen UI-Login und läuft nur beim ersten Aufruf oder wenn die Session ungültig geworden ist. Die optionale validate-Funktion prüft vor jeder Wiederverwendung, ob die gecachte Session noch gültig ist, etwa durch einen leichten API-Call gegen einen geschützten Endpunkt. Mit cacheAcrossSpecs: true bleibt die Session sogar über mehrere Spec-Dateien hinweg erhalten, was besonders bei Magento-Shops mit trägem Customer-Login spürbar Laufzeit spart.


// cypress/support/commands.js

Cypress.Commands.add('login', (email, password) => {
  cy.session(
    [email, password],
    () => {
      // Runs only once per unique session id, real UI login
      cy.visit('/customer/account/login');
      cy.get('#email').type(email);
      cy.get('#pass').type(password, { log: false });
      cy.get('#send2').click();
      cy.url().should('include', '/customer/account');
    },
    {
      cacheAcrossSpecs: true,
      validate() {
        // Cheap check to confirm the cached session is still valid
        cy.request('/customer/section/load/?sections=customer')
          .its('body.customer.firstname')
          .should('exist');
      },
    }
  );
  cy.visit('/customer/account');
});

4. Add-to-Cart-Command für einen Magento-Shop

Der Warenkorb-Flow eines Magento-Shops besteht typischerweise aus mehreren Teilschritten: Produktseite öffnen, optional Menge anpassen, Konfigurationsoptionen wählen, Submit-Button klicken und die Aktualisierung des Mini-Cart-Badges abwarten. Ein cy.addToCart(sku, options)-Command bündelt genau diese Schritte hinter einer fachlichen Signatur, ohne dabei die einzelnen Zwischenschritte im Command Log zu verstecken, da jeder verschachtelte cy.get()-Aufruf weiterhin als eigener Log-Eintrag erscheint.

Ein Optionen-Objekt mit sinnvollen Defaults, { qty: 1, viaMiniCart: false }, hält den Aufruf im Regelfall kurz, cy.addToCart('24-MB01'), erlaubt aber gezielte Abweichungen für Edge-Case-Tests. Wichtig ist, dass der Command am Ende ein sinnvolles Subject zurückgibt, etwa das Mini-Cart-Badge-Element, damit der aufrufende Test direkt weiter chainen kann, cy.addToCart(sku).should('contain', '1'), statt danach erneut manuell nach dem Element suchen zu müssen.


// cypress/support/commands.js

Cypress.Commands.add('addToCart', (sku, options = {}) => {
  const { qty = 1, viaMiniCart = false } = options;

  cy.visit(`/catalog/product/view/sku/${sku}`);
  cy.get('#qty').clear().type(String(qty));
  cy.get('#product-addtocart-button').click();

  // Wait for the AJAX add-to-cart response before asserting anything
  cy.wait('@addToCartRequest');

  if (viaMiniCart) {
    cy.get('[data-testid="minicart-icon"]').click();
  }

  // Yield the mini-cart badge so callers can chain assertions directly
  return cy.get('[data-testid="minicart-qty-badge"]');
});

5. TypeScript-Typing: index.d.ts und declare global

Ohne explizites Typing kennt TypeScript cy.login() oder cy.addToCart() nicht, jeder Aufruf erzeugt einen Compile-Fehler, sobald das Projekt strikte Typprüfung nutzt. Die Lösung ist Declaration Merging: In einer cypress/support/index.d.ts wird der bestehende Cypress-Namespace über declare global und declare namespace Cypress um zusätzliche Methoden im Chainable-Interface erweitert, ohne den originalen Typ zu überschreiben.

Jede Methode sollte einen JSDoc-Kommentar mit Kurzbeschreibung und einem @example-Block erhalten, denn diese Kommentare erscheinen direkt in der Autocomplete-Vorschau der IDE und ersetzen faktisch eine separate Command-Dokumentation. Generische Rückgabetypen wie Chainable> für DOM-Elemente oder Chainable für reine Aktions-Commands verhindern, dass falsche Folgeaufrufe wie .type() auf einem Command ohne DOM-Subject unbemerkt durchrutschen.


// cypress/support/index.d.ts

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      /**
       * Logs a customer in via a cached UI session (cy.session).
       * @example cy.login('jane@example.com', 'secret123')
       */
      login(email: string, password: string): Chainable<void>;

      /**
       * Adds a product to the cart and yields the mini-cart badge element.
       * @example cy.addToCart('24-MB01', { qty: 2 })
       */
      addToCart(
        sku: string,
        options?: { qty?: number; viaMiniCart?: boolean }
      ): Chainable<JQuery<HTMLElement>>;

      /**
       * Child command: finds a descendant by data-testid attribute.
       * @example cy.get('.cart').getBySel('cart-badge')
       */
      getBySel(selector: string): Chainable<JQuery<HTMLElement>>;
    }
  }
}

export {};

6. Command-Chaining und Subjects korrekt yielden

Cypress-Commands sind keine synchronen Funktionsaufrufe, sondern Einträge in einer intern verwalteten Kommandowarteschlange, die erst asynchron abgearbeitet wird. Ein häufiger Anfängerfehler in Custom Commands ist, innerhalb der Callback-Funktion einen Wert per return aus einem then() zurückzugeben, ohne zu beachten, dass Cypress bei fehlendem explizitem Return automatisch das Subject des letzten Cypress-Befehls in der Funktion weiterreicht. Wer stattdessen versehentlich einen rohen JavaScript-Wert zurückgibt, bricht die Chain und macht nachfolgende .should()-Aufrufe unvorhersehbar.

Die verlässliche Regel: Ein Custom Command sollte entweder explizit cy.wrap(value) zurückgeben oder implizit das Ergebnis des letzten Cypress-Befehls in der Funktion weiterreichen, niemals eine rohe Promise oder einen unverpackten Wert. Für Commands, die mehrere unabhängige Cypress-Befehle ausführen, aber kein sinnvolles Subject für Folgeaufrufe haben, ist Chainable als Rückgabetyp ehrlicher als ein Subject vorzutäuschen, das ins Leere chained.

7. Commands überschreiben und Optionen-Objekte

Cypress.Commands.overwrite() ersetzt das Verhalten eines bestehenden Commands, eigener oder eingebauter, unter Beibehaltung seiner Aufrufsignatur. Ein typischer Einsatzfall in Magento-Projekten: cy.visit() überschreiben, um bei jedem Aufruf automatisch einen Test-Header mitzuschicken, der den Full Page Cache umgeht, ohne dass jeder einzelne Test diesen Header manuell setzen muss. Der überschriebene Command erhält als ersten Parameter die ursprüngliche Implementierung und muss diese explizit aufrufen, sonst geht das native Verhalten komplett verloren.

Bei eigenen Commands mit vielen Varianten empfiehlt sich ein einzelnes Optionen-Objekt statt vieler Positionsparameter, cy.addToCart(sku, { qty, viaMiniCart }) statt cy.addToCart(sku, qty, viaMiniCart, false, true). Optionen-Objekte mit sinnvollen Defaults bleiben lesbar, auch wenn später weitere Flags dazukommen, während positionsbasierte Parameter bei jeder Erweiterung alle bestehenden Aufrufstellen syntaktisch gefährden.

8. Über-Abstraktion vermeiden: wenn Commands Tests unlesbar machen

Der Reiz von Custom Commands verleitet dazu, immer mehr Logik in immer größere Commands zu packen, bis ein einzelner Aufruf wie cy.checkout() zwölf UI-Schritte, drei Assertions und zwei API-Waits verschluckt. Das Problem zeigt sich beim Debuggen eines fehlschlagenden Tests: Der Command Log zeigt zwar an, dass checkout fehlgeschlagen ist, aber welcher der zwölf Schritte konkret, ist ohne Aufklappen der internen Implementierung nicht erkennbar. Der Test selbst verliert außerdem seine Aussagekraft als lesbare Spezifikation, weil die eigentliche Fachlogik im Support-Ordner versteckt ist statt im Testfall sichtbar zu sein.

Die praktikable Faustregel: Ein Custom Command sollte einen einzigen fachlichen Schritt oder eine einzige technische Operation abbilden, Login, Add-to-Cart, ein Formularfeld ausfüllen, nicht einen kompletten mehrstufigen User-Flow. Mehrstufige Flows gehören als Abfolge mehrerer Custom Commands in den Test selbst, cy.login(); cy.addToCart(sku); cy.goToCheckout(); cy.fillShippingAddress(data);, damit jeder Schritt einzeln im Command Log nachvollziehbar bleibt und ein Testfehler sofort die richtige Stelle zeigt.


// BAD: over-abstracted command hides the entire checkout flow
Cypress.Commands.add('checkout', (sku, address, payment) => {
  cy.addToCart(sku);
  cy.get('[data-testid="checkout-btn"]').click();
  cy.get('#firstname').type(address.firstname);
  cy.get('#lastname').type(address.lastname);
  cy.get('#street').type(address.street);
  cy.get('[data-testid="shipping-continue"]').click();
  cy.get(`[data-testid="payment-${payment}"]`).click();
  cy.get('[data-testid="place-order"]').click();
  cy.get('.checkout-success').should('be.visible');
  // A failure here gives almost no clue which of the 8 steps broke
});

// GOOD: focused commands, the test itself stays a readable spec
cy.login('jane@example.com', 'secret123');
cy.addToCart('24-MB01', { qty: 2 });
cy.goToCheckout();
cy.fillShippingAddress(shippingFixture);
cy.selectPaymentMethod('checkmo');
cy.placeOrder();
cy.get('.checkout-success').should('be.visible');

9. Custom Commands im direkten Vergleich

Nicht jeder Custom Command ist automatisch eine Verbesserung. Die folgende Übersicht zeigt typische Entscheidungen beim Design von Custom Commands und welche Variante sich in der Praxis als wartbarer erwiesen hat.

Aufgabe Naiver Ansatz Empfohlenes Pattern Vorteil
Login in jedem Test UI-Login inline in jeder Spec kopiert cy.login() mit cy.session Ein Ort für Änderungen, kein UI-Login-Overhead pro Test
Selektor-Wiederholung cy.get('[data-testid=x]') überall dupliziert Child-Command cy.getBySel() Selektor-Strategie zentral austauschbar
Warenkorb-Schritte PDP, Menge, Submit manuell in jedem Test cy.addToCart(sku, options) Fachlicher Aufruf, Schritte bleiben im Log sichtbar
Kompletter Checkout Ein cy.checkout() mit 12 versteckten Schritten Kette fokussierter Commands im Test Fehler zeigt sofort den richtigen Schritt
Built-in-Verhalten anpassen cy.visit komplett neu nachgebaut Cypress.Commands.overwrite('visit', ...) Natives Verhalten bleibt erhalten, nur gezielt erweitert

Mironsoft

E2E-Testautomatisierung und Cypress-Setups für Magento- und Hyvä-Shops

Eine Cypress-Suite, der euer Team wirklich vertraut?

Wir bauen wartbare Custom-Command-Bibliotheken mit sauberem TypeScript-Typing, cy.session-Login und klar geschnittenen Commands für euren Magento-Shop, statt einer Blackbox aus über-abstrahierten Testschritten.

Command-Bibliothek Audit

Bestehende Custom Commands auf Granularität, Typing und Chaining prüfen

TypeScript-Setup

index.d.ts, Declaration Merging und JSDoc-Autocomplete einrichten

CI-Integration

cy.session-Caching, Parallelisierung und stabile Pipelines aufbauen

10. Zusammenfassung

Cypress Custom Commands lösen das Grundproblem jeder wachsenden E2E-Suite: wiederkehrende Abläufe wie Login und Add-to-Cart sollen an genau einer Stelle definiert sein, nicht in Dutzenden Spec-Dateien kopiert. Cypress.Commands.add() unterscheidet Parent-, Child- und Dual-Commands über die prevSubject-Option, cy.session() cacht Login-Zustand und spart Laufzeit, und ein sauberes index.d.ts mit Declaration Merging macht Commands in TypeScript vollständig typsicher und autocomplete-fähig.

Der größte Stolperstein ist nicht die Technik, sondern die richtige Granularität: Ein Custom Command sollte einen fachlichen Schritt kapseln, nicht einen kompletten mehrstufigen Flow. Wer stattdessen zwölf UI-Schritte in einem einzigen cy.checkout() versteckt, tauscht kurzfristige Kürze gegen langfristig unlesbare, schwer debugbare Tests. Fokussierte Commands, korrektes Subject-Yielding und Cypress.Commands.overwrite() für gezielte Anpassungen eingebauter Befehle bilden zusammen die Basis einer Testsuite, die mit dem Projekt mitwächst statt gegen es zu arbeiten.

Cypress Custom Commands, Das Wichtigste auf einen Blick

Command-Typen

Parent startet die Chain, Child verarbeitet ein Subject, Dual funktioniert in beiden Rollen über prevSubject.

Login-Caching

cy.session() cacht Cookies und Storage, spart UI-Login pro Test, validate() prüft Gültigkeit.

TypeScript-Typing

index.d.ts mit declare global und Declaration Merging für vollständige Autocomplete-Unterstützung.

Richtige Granularität

Ein Command, ein fachlicher Schritt. Mehrstufige Flows bleiben als Command-Kette im Test sichtbar.

11. FAQ: Cypress Custom Commands

1Was ist ein Cypress Custom Command?
Eine über Cypress.Commands.add() registrierte, wiederverwendbare Funktion, die projektweit als cy.methodenname() verfügbar ist und wiederkehrende Abläufe an einer Stelle kapselt.
2Wann Parent- und wann Child-Command?
Parent-Commands starten eine neue Chain ohne Subject. Child-Commands mit prevSubject: true verarbeiten das Subject des vorherigen Befehls.
3Was macht cy.session so wichtig?
Cacht Cookies und Storage nach dem ersten UI-Login und stellt sie bei weiteren Aufrufen sofort wieder her, ohne erneuten UI-Durchlauf.
4Wie type ich Commands in TypeScript?
Über Declaration Merging in index.d.ts: declare global und declare namespace Cypress erweitern das Chainable-Interface um eigene Methoden.
5Warum cy.get() nicht überall wiederholen?
Selektoren ändern sich bei UI-Änderungen. Ein Child-Command wie getBySel() zentralisiert die Selektor-Strategie an einer Stelle.
6Wann ist ein Command über-abstrahiert?
Wenn er mehrere unabhängige Schritte bündelt, etwa ein zwölfstufiges cy.checkout(). Ein Fehler zeigt dann nicht mehr, welcher Teilschritt scheiterte.
7Kann ich cy.visit überschreiben?
Ja, mit Cypress.Commands.overwrite('visit', fn). Die ursprüngliche Implementierung muss explizit aufgerufen werden, sonst geht natives Verhalten verloren.
8Wie chaine ich Commands korrekt?
Explizit cy.wrap(value) zurückgeben oder implizit das Ergebnis des letzten Cypress-Befehls weiterreichen. Rohe Werte oder Promises brechen die Chain.
9Sollten Assertions in Commands stehen?
Nur technische Voraussetzungs-Assertions. Fachliche Assertions über das erwartete Testergebnis gehören sichtbar in den Testfall.
10Eigene Command-Bibliothek pro Projekt?
Generische Commands wie getBySel eignen sich für ein npm-Paket. Fachliche Commands wie addToCart bleiben meist projektspezifisch.