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.
Inhaltsverzeichnis
- 1. Warum Custom Commands in Cypress-Tests unverzichtbar sind
- 2. Cypress.Commands.add(): Parent-, Child- und Dual-Commands
- 3. Login-Command mit cy.session bauen
- 4. Add-to-Cart-Command für einen Magento-Shop
- 5. TypeScript-Typing: index.d.ts und declare global
- 6. Command-Chaining und Subjects korrekt yielden
- 7. Commands überschreiben und Optionen-Objekte
- 8. Über-Abstraktion vermeiden: wenn Commands Tests unlesbar machen
- 9. Custom Commands im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.