Cypress.io (oder kurz Cypress) bekommt momentan sehr viel Aufmerksamkeit, wenn es um das Thema End-to-End-Testing geht. Speziell im JavaScript-Umfeld scheint sich Cypress.io langsam durchzusetzen. Es macht vieles richtig und ist Selenium-basierten Ansätzen meiner Ansicht nach vorzuziehen.
Im Bereich Frontend-Test habe ich in letzter Zeit gute Erfahrungen mit Cucumber.js und einem BDD -basierten Ansatz gemacht. Dort stand für mich aber bisher weniger der End-to-End-Test -Aspekt im Fokus, sondern war eher das natürlichsprachliche Beschreiben der clientseitigen Logik im Vordergrund. Bei einer komplexen clientseitigen Fachlichkeit und einem Offline-First-Ansatz funktionierte dies exzellent. Das Test-Setup wurde selten über die UI, sondern eher über den State der Anwendung gemacht. Das Testen des Resultats dann oft über die UI. Der Zugriff auf UI-Elemente erfolgte dabei über DOM-API (jsdom ) bzw. Enzyme . Es gab bei diesem Vorgehen wenig Probleme beim Zugriff auf UI-Elemente (wie man es doch oft von Selenium kennt) und war ausreichend schnell.
Allerdings fühlte sich der Zugriff über die DOM-API doch manchmal etwas low-levellig an und als ich Cypress.io und die entsprechende Cucumber-Erweiterung gesehen habe, war ich doch neugierig, ob man mit diesem Toolset dieses Vorgehen nicht noch verfeinern kann.
Die Beispielanwendung
Für diesen Zweck habe ich eine sehr kleine Beispielanwendung geschrieben, in der ein Händler von Kaffeebohnen diese verwalten kann.
Es ist eine React-Anwendung (basierend auf Create-React-App ), die kein Backend hat. Sie enthält aber clientseitig eine (noch) recht einfache Logik um Preise zu berechnen, die aber schon Margen und Rabatte berücksichtigt. Dies simuliert uns den Offline-First-Fall. Die Sourcen der Anwendung findet man hier .
Aufsetzen von Cypress.io
Ein yarn add cypress installiert uns wie gewohnt Cypress. Auch wie gewohnt ergänze ich Skripte in der package.json
1"scripts": { 2 ... 3 "cypress:open": "cypress open", 4 "cypress:run": "cypress run", 5 ... 6}
Der erste Aufruf von Cypress über yarn cypress open
initialisiert und öffnet Cypress. Ich verwende yarn in diesem Beispiel, npm funktioniert hier aber genauso gut.
Die Initialisierung erzeugt aber auch die Cypress-Ordnerstrukur.
1cypress 2├── fixtures 3├── integration 4├── plugins 5├── screenshots 6├── support 7└── videos
Aufsetzen des cypress-cucumber-preprocessor
Soweit so bekannt. Als nächstes installieren wir die Cucumber-Erweiterung für Cypress – cypress-cucumber-preprocessor – um Cucumber-Tests mit Cypress verwenden zu können. Dieser ist als Cypress Preprocessor implementiert und wandelt unsere Cucumber-Tests vor der Testausführung in Cypress-Tests um.
yarn add cypress-cucumber-preprocessor
Dann registrieren wir den Preprozessor unter cypress/plugins/index.js
1const cucumber = require('cypress-cucumber-preprocessor').default
2module.exports = (on, config) => {
3 on('file:preprocessor', cucumber())
4}
Mehr ist an Konfiguration zunächst mal nicht nötig.
Ein Cucumber-Test
Dann können wir auch schon loslegen und unser erstes Cucumber-Feature schreiben. Die Features legen wir unter cypress/integration/ ab.
#language: de
Funktionalität: Rabatt für eine Bohnenart berechnen
Als Bohnenverkäufer möchte ich einen Rabatt gewähren können:
Grundlage:
Angenommen die Anwendung ist geöffnet:
Szenario: Ein neuer Rabatt soll gewährt werden
Angenommen es gibt folgende Bohnenarten in der Anwendung:
| Id | Bohne | Einkaufspreis in Euro | Marge in Prozent | Verkaufspreis in Euro | Rabatt |
| 1 | Äthiopien | 10.00 | 30 | 13.00 | 0.00 |
Wenn der Bohnenverkäufer einen Rabatt von "10" Prozent gewährt
Dann ist der Rabatt von "10" Prozent in der Anwendung sichtbar
Und ist der Verkaufspreis mit Rabatt '11.70' Euro
Und ist der Verkaufspreis '13.00' Euro
Das Feature an sich ist wahrscheinlich schon so verständlich. Jemand mit Cucumber-Vorwissen wird auch nicht überrascht werden.
Das Feature ist so natürlich noch nicht lauffähig, sondern es bedarf noch etwas Code zur Implementierung der einzelnen Schritte: die Step Definitions.
Schauen wir uns die Schritte mal im Einzelnen an. Zunächst sagen wir, dass wir Cucumber gerne auf Deutsch verwenden möchten #language: de
. Auch die Funktionalität bedarf keiner Implementierung, da diese rein informativ ist.
Spannender wird es dann bei der Grundlage. Dies ist ein Schritt, der vor jedem Szenario einmal ausgeführt werden soll. Hier sagen wir, dass die Anwendung geöffnet sein soll, als Grundlage für unsere Szenarien. So müssen wir das nicht in jedem Szenario wieder bauen. Dies ist ein Schritt, der wahrscheinlich von vielen Features gebraucht wird, und wir sehen diesen als Common Code an.
Wir legen also unter cypress/integration/common/ eine Datei mit dem Namen global.js
ab. Der Ort ist ein Default, ist aber konfigurierbar. Wie die Auflösung von Steps zu Implementierung aussieht, werde ich später noch etwas näher erläutern. Sowohl der Pfad zu den Step Definitions als auch der Pfad zum Common-Code ist natürlich konfigurierbar.
1import {Given as Angenommen} from 'cypress-cucumber-preprocessor/steps';
2
3export function oeffneAnwendung() {
4 cy.visit('/');
5}
6
7Angenommen('die Anwendung ist geöffnet', function () {
8 oeffneAnwendung();
9});
Als erstes wird also ein cy.visit(‘/’)
aufgerufen. Dies kennt man wahrscheinlich aus seinen Cypress-Tests.
Schauen wir uns dann das Szenario an. Auch hier brauchen wir keine Implementierung. Dies hat wieder beschreibende Funktion. Im ersten Step des Szenarios setzen wir Testdaten. Ein Arrange-Schritt, wie man es z.B. aus JUNit- oder Jest-Tests kennt.
Angenommen es gibt folgende Bohnenarten in der Anwendung:
| Id | Bohne | Einkaufspreis in Euro | Marge in Prozent | Verkaufspreis in Euro | Rabatt |
| 1 | Äthiopien | 10.00 | 30 | 13.00 | 0.00 |
Wie ich in der Einleitung beschrieben habe, möchte ich dieses Aufbauen der Testdaten nicht über die UI machen. Hier habe ich mich für den Weg entschieden, eine Redux Action zu nutzen, um Daten in das System zu laden.
Auch diesen Schritt habe ich in die global.js
gelegt, da ich ihn in vielen Fällen brauchen werde:
1Angenommen('es gibt folgende Bohnenarten in der Anwendung', function (dataTable) {
2 for (const row of dataTable.hashes()) {
3 const id = row['Id'];
4 const art = row['Bohne'];
5 const ekp = row['Einkaufspreis in Euro'];
6 const vkp = row['Verkaufspreis in Euro'];
7 const marge = row['Marge in Prozent'];
8 cy
9 .window()
10 .its('store')
11 .invoke('dispatch', updateData({id, art, ekp, vkp, marge}));
12 }
13});
Das erste was man hier sieht, ist die DataTable API , die uns hier aber nicht weiter interessiert. Wir lesen Zeile für Zeile unsere Tabelle aus dem Feature ein und lösen eine Redux Action aus.
Hierfür ist es nötig, dass Cypress Zugriff auf den Redux-Store hat. Ich habe mich hier dazu entschieden, den Redux-Store an das window-Objekt zu kleben, und kann dann ein (Redux -) Dispatch auslösen.
Im nächsten Schritt wollen wir einen neuen Rabatt setzen.
Wenn der Bohnenverkäufer einen Rabatt von "10" Prozent gewährt
Dies soll über die UI passieren. Da kann Cypress nun wirklich seine Stärken ausspielen: Es ist zunächst mal eine Funktionalität, die nur für das Rabatt-Feature interessant ist. Wir legen eine Datei rabatt.js
unter cypress/integration/rabatt an.
1Wenn('der Bohnenverkäufer einen Rabatt von {string} Prozent gewährt', function (rabatt) {
2 cy.get('#rabatt').type(`{selectall}{backspace}${rabatt}`);
3});
Cucumber stellt uns hier den im Feature definierten Rabatt zur Verfügung und wir tippen diesen Rabatt ein (und löschen eine mögliche vorherige Eingabe). Der Einfachheit halber benutze ich hier die ID des Feldes.
Nun müssen wir noch überprüfen, ob die Eingabe auch geklappt hat und die Anwendung richtig rechnet.
Dann ist der Rabatt von "10" Prozent in der Anwendung sichtbar.
Und ist der Verkaufspreis mit Rabatt '11.70' Euro.
Auch in der rabatt.js hinterlegen wir:
1Dann('ist der Rabatt von {string} Prozent in der Anwendung sichtbar', function (rabatt) {
2 cy.get('#rabatt').should('have.value', rabatt);
3});
4Dann('ist der Verkaufspreis mit Rabatt {string} Euro', function (vkpRabatt) {
5 cy.get('#vkpRabatt').should('have.value', vkpRabatt);
6});
Das wars auch schon!
Wo liegen die Step-Definitionen?
Wie oben schon angedeutet, bringt der cypress-cucumber-preprocessor im Default das Verhalten mit, dass die Step-Definition zu einem Feature in einem gleichnamigen Verzeichnis zu liegen hat und das Feature auch nur auf diese Step-Definitionen zurückgreift (und zusätzlich auf solche, die ich im common-Verzeichnis abgelegt habe). Dies ist zunächst einmal ungewöhnlich, wenn man schon mit Cucumber.js gearbeitet hat. Dort hat man im Default diese Einschränkung nicht.
Das Fehlen dieser Einschränkung macht es viel einfacher, eine Step-Bibliothek aufzubauen, da es einfacher ist, Steps wiederzuverwenden. Problematisch wird es natürlich, falls Duplikate auftreten und/oder ein Step Zoo entsteht. Die Maintainer des cypress-cucumber-preprocessor geben auch noch Performance-Gründe an.
Wie dem auch sei, auch dieses Verhalten lässt sich konfigurieren.
In der package.json
:
1"cypress-cucumber-preprocessor": { 2 "nonGlobalStepDefinitions": false 3}
Testausführung
Aber führen wir doch unsere Tests aus. Wir starten unseren Server mit yarn start
. Natürlich spricht auch nichts gegen ein eingebautes start-server -Skript, wie es vielleicht schon von Cypress in einer CI-Umgebung bekannt ist. Ist hier aber nicht unbedingt nötig.
Dann können wir unsere Tests wie gehabt entweder headless mit yarn cypress:run
oder mit sichtbarer Testausführung mit yarn cypress:open
ausführen.
Wenn wir Cypress über yarn cypress:open
öffnen, sehen wir, dass sich die Cucumber-Tests in der Ausführung gar nicht so sehr von den gewohnten Cypress-Tests unterscheiden:
Im Headless-Mode sieht dies ähnlich aus:
1$ cypress run 2 3==================================================================================================== 4 5 (Run Starting) 6 7 ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ 8 │ Cypress: 3.2.0 │ 9 │ Browser: Electron 59 (headless) │ 10 │ Specs: 1 found (rabatt.feature) │ 11 └────────────────────────────────────────────────────────────────────────────────────────────────┘ 12 13──────────────────────────────────────────────────────────────────────────────────────────────────── 14 15 Running: rabatt.feature... (1 of 1) 16 17 18 Rabatt für eine Bohnenart berechnen 19 ✓ Ein neuer Rabatt soll gewährt werden (1009ms) 20 21 1 passing (1s) 22 23 24 (Results) 25 26 ┌──────────────────────────────┐ 27 │ Tests: 1 │ 28 │ Passing: 1 │ 29 │ Failing: 0 │ 30 │ Pending: 0 │ 31 │ Skipped: 0 │ 32 │ Screenshots: 0 │ 33 │ Video: true │ 34 │ Duration: 1 second │ 35 │ Spec Ran: rabatt.feature │ 36 └──────────────────────────────┘ 37 38 (Video) 39 40 - Started processing: Compressing to 32 CRF 41 - Finished processing: /Users/holgergp/development/sideprojects/cypress.io/cypressioCucumberExample/cypress/videos/rabatt.feature.mp4 (0 seconds) 42==================================================================================================== 43 44 (Run Finished) 45 Spec Tests Passing Failing Pending Skipped 46 ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ 47 │ ✔ rabatt.feature 00:01 1 1 - - - │ 48 └────────────────────────────────────────────────────────────────────────────────────────────────┘ 49 All specs passed! 00:01 1 1 - - - 50 51✨ Done in 16.85s.
Wir können die Cucumber-Tests also genauso ausführen wie wir es gewohnt sind. Super!
Falls man irgendwann eine größere Cucumber-Testsuite hat, kann die Laufzeit aller Tests bei der Entwicklung vielleicht hinderlich sein. Hier bietet schon Cucumber.js die Möglichkeit, Tests zu taggen , um z.B. nur nur Tests einer bestimmten Fachlichkeit ausführen zu können.
Hier kommt der cypress-cucumber-preprocessor mit einem netten Feature: Er liefert den Tag @focus
mit, der Cypress nur die aktuell betrachteten Tests ausführen lässt.
@focus
Szenario: Ein weiterer Rabatt soll gewährt werden
Angenommen, es gibt folgende Bohnenarten in der Anwendung:
| Id | Bohne | Einkaufspreis in Euro | Marge in Prozent | Verkaufspreis in Euro | Rabatt |
| 1 | Äthiopien | 10.00 | 30 | 13.00 | 0.00 |
Wenn der Bohnenverkäufer einen Rabatt von "20" Prozent gewährt
Dann ist der Rabatt von "20" Prozent in der Anwendung sichtbar
Fehler
Erfolgreiche Tests sind nun das eine. Tests sollen mir aber auch helfen ein Fehlverhalten aufzudecken. Sie sollen also auch mal fehlschlagen, dann aber auch sinnvolle Fehlermeldungen liefern, so dass ich den Fehler schnell beheben kann.
Wenn wir mal hingehen und einen der Tests fehlerhaft anpassen:
Szenario: Ein weiterer Rabatt soll gewährt werden
Angenommen es gibt folgende Bohnenarten in der Anwendung:
| Id | Bohne | Einkaufspreis in Euro | Marge in Prozent | Verkaufspreis in Euro | Rabatt |
| 1 | Äthiopien | 10.00 | 30 | 13.00 | 0.00 |
Wenn der Bohnenverkäufer einen Rabatt von "20" Prozent gewährt
Dann ist der Rabatt von "30" Prozent in der Anwendung sichtbar
In der Cypress-Umgebung sehen wir diesen Fehler recht schön und es ist ersichtlich, was ich anpassen muss. Im Headless-Mode ist dieser Fehler etwas technischer, woran man sich vielleicht erst gewöhnen muss. Dies stellt aber aus meiner Sicht kein größeres Problem dar.
1Rabatt für eine Bohnenart berechnen 2 1) Ein weiterer Rabatt soll gewährt werden: 3 4 0 passing (5s) 5 1 failing 6 7 1) Rabatt für eine Bohnenart berechnen Ein weiterer Rabatt soll gewährt werden: 8 CypressError: Timed out retrying: expected '<input#rabatt>' to have value '30', but the value was '20' 9 at Object.cypressErr (http://localhost:3000/__cypress/runner/cypress_runner.js:65727:11) 10 at Object.throwErr (http://localhost:3000/__cypress/runner/cypress_runner.js:65692:18) 11 at Object.throwErrByPath (http://localhost:3000/__cypress/runner/cypress_runner.js:65719:17) 12 at retry (http://localhost:3000/__cypress/runner/cypress_runner.js:59237:16) 13 at http://localhost:3000/__cypress/runner/cypress_runner.js:51312:18 14 at tryCatcher (http://localhost:3000/__cypress/runner/cypress_runner.js:131273:23) 15 at Promise._settlePromiseFromHandler (http://localhost:3000/__cypress/runner/cypress_runner.js:129291:31) 16 at Promise._settlePromise (http://localhost:3000/__cypress/runner/cypress_runner.js:129348:18) 17 at Promise._settlePromise0 (http://localhost:3000/__cypress/runner/cypress_runner.js:129393:10) 18 at Promise._settlePromises (http://localhost:3000/__cypress/runner/cypress_runner.js:129468:18) 19 at Async._drainQueue (http://localhost:3000/__cypress/runner/cypress_runner.js:126197:16) 20 at Async._drainQueues (http://localhost:3000/__cypress/runner/cypress_runner.js:126207:10) 21 at Async.drainQueues (http://localhost:3000/__cypress/runner/cypress_runner.js:126081:14) 22 at <anonymous>
Fazit
Der cypress-cucumber-preprocessor macht mir echt Spaß. Die Integration von Cucumber in die Cypress.io-Umgebung ist gelungen und ist aus meiner Sicht reif, auch produktiv eingesetzt zu werden. Der elegante Weg, wie man mit Cypress auf UI-Elemente zugreift, hilft auch, Cucumber Step-Definitonen ausdrucksstärker zu gestalten.
Falls man Cucumber.js schon stand-alone kennt, muss man sich an der ein oder anderen Stelle vielleicht umgewöhnen. Das Handhaben der Step-Definitionen und das Bereitstellen des Redux-Stores wären hier als Beispiel genannt.
Ich habe Cucumber.js auch bisher weniger im Kontext E2E eingesetzt, sondern eher als Werkzeug um komplexe clientseitige Anwendungen zu testen, wobei der Server-Part bewusst außen vor gelassen wurde. Dies macht auch nach wie vor für mich Sinn, heißt im Umkehrschluss: Nur für Cucumber würde ich Cypress.io nicht ins Boot holen.
Falls ich allerdings Cypress.io sowieso schon an Bord haben und/oder ich E2E-Tests formulieren möchte, so ist dies ein exzellenter Weg, auch BDD-Tests zu verwenden.
Viel Spaß mit Cypress.io und Cucumber!
Weitere Beiträge
von Holger Grosse-Plankermann
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Holger Grosse-Plankermann
Senior IT Consultant | Senior Developer
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.