Im Bereich der End-to-End-Tests hat das Open-Source-Werkzeug Cypress [Cyp] einen etablierten Platz (siehe auch [npm]). Dies liegt vor allem an einigen Features, die das Leben eines Testers deutlich erleichtern (vgl. auch [BrSo21]). So laufen die Tests in der Regel stabil und zuverlässig durch das automatische Warten auf das Erscheinen oder Verschwinden eines Elements im UI, weitere Vorteile sind das Tippen in (schneller) Nutzergeschwindigkeit in Eingabefeldern oder die Möglichkeit, die Zeit per Clock zu manipulieren. Die Fehlersuche wird erleichtert durch das sogenannte Daumenkino, das es erlaubt, den Testfall in seinem Verlauf als DOM-Snapshots und den Zustand vor und nach jeder Aktion nachzuvollziehen.
Netzwerkverkehr kann mit Cypress E2E gemockt werden, wobei es sich dann nicht mehr um einen klassischen End-to-End-Test, sondern eher um einen Integrationstest handelt. Ist die Cypress-Testumgebung erst einmal gestartet, laufen die Tests sehr schnell, da sie die zu testende Anwendung direkt und nicht über den Umweg über einen Browser steuern und manipulieren.
Gleichzeitig hat Cypress im End2End-Test-Bereich auch einige Nachteile. So gibt es aufgrund des technischen Aufbaus einige Limitierungen, da Cypress mit der zu testenden Anwendung im Browser und die zu testende Anwendung dabei in einem iFrame läuft. Multi-Tab-Anwendungen lassen sich naturgemäß nicht testen, iFrames in der zu testenden Anwendung nur auf Umwegen.
Mit Cypress Component Testing wird nicht die gesamte Anwendung auf höherer Teststufe getestet, sondern einzelne Teile der Anwendung. In der Testpyramide sind das die Teststufen Unittests, Komponententests und Integrationstests von Frontend-Komponenten (siehe Abbildung 1).
Abb. 1: Zuordnung der Cypress-Werkzeuge zu Teststufen
Wir verstehen dabei unter Komponententest den Test abgegrenzter UI-Komponenten, bei denen jedoch anders als im Unittest aus pragmatischen Gründen nicht alle Abhängigkeiten gemockt sind – so ist es oft nicht sinnvoll, eine äußere Komponente ohne ihre inneren Komponenten zu testen. Die Grenze zum Integrationstest ist dabei fließend.
In diesen unteren Teststufen sind die oben genannten Einschränkungen nicht relevant, da in der Regel die UI-Komponenten als kleinere Einheiten getestet werden und somit Multi-Tabbing und iFrames keine Rolle spielen. Auch dass als Sprache für die Tests ausschließlich JavaScript beziehungsweise TypeScript verwendet werden kann, ist für Web-Entwickler kein Problem. Diese Einschränkung stellt für die unteren Teststufen kein Hindernis dar, im Gegensatz zum End-to-End-Test, wo Testfälle oft auch von Testern ohne größere Entwicklungserfahrung erstellt werden.
Startbereit machen
Bevor man Cypress nutzen kann, muss es zunächst als Paket in das Frontend-Projekt integriert werden, in der Regel mit einem Package-Manager wie npm oder yarn. Will man Cypress nur einmal ausprobieren, so lässt es sich auch direkt herunterladen und auspacken, dies ist jedoch nicht der empfohlene Weg.
Beim ersten Starten muss Cypress konfiguriert werden. Dabei kann man entweder nur Cypress E2E oder nur Cypress Component Testing oder beides nutzen. Es ist danach jederzeit möglich, in der Cypress-Testumgebung zwischen den beiden Testtypen hin und her zu wechseln (siehe Abbildung 2).
Abb. 2: Cypress-Testumgebung mit Auswahl der Testart
In unserem Fall haben wir beide Testtypen konfiguriert. Dabei muss Cypress für das Component Testing das für die Anwendung genutzte Framework unterstützen, da Cypress wissen muss, wie die UI-Komponenten als einzelne Einheiten zu mounten sind. Cypress stellt inzwischen Support für eine Reihe von Web-Frameworks/-Bibliotheken zur Verfügung, darunter die weitverbreiteten Frameworks/Bibliotheken für Single-Page-Anwendungen React, Angular und Vue.
Cypress legt End-to-End-Tests in der Standardkonfiguration in einem eigenen Verzeichnis unabhängig vom Produktivcode ab, während für das Component Testing jeder Testfall direkt neben der zu testenden Komponente abgelegt wird, wie es im Web-Frontend auch bei anderen Werkzeugen für die unteren Teststufen üblich ist. Wenn wir also gleich Komponententests schreiben werden, dann in Dateien, die analog zur zu testenden Komponente mit dem Suffix .spec.tsx benannt sind.
Komponententests mit Cypress
Um das Vorgehen zu demonstrieren, haben wir eine kleine Beispielanwendung testgetrieben mit Cypress Component Testing entwickelt1. Die Anwendung besteht aus einer Anzeigekomponente für Buchungen, sortiert nach Tagen (siehe Abbildung 3).
Abb. 3: Beispielanwendung zur Verwaltung von Buchungen
Das Frontend ist in React implementiert, als Backend fungiert ein Node.js-Server. Im gezeigten Zustand können die Einträge nur gelöscht werden. Die Anwendung soll nun um eine Eingabekomponente zur Anlage neuer und Änderung bestehender Buchungen erweitert werden. Auch in realen Kundenprojekten setzen wir Cypress auf die vorgestellte Weise ein, darüber hinaus auch für End2End-Tests.
Zunächst schauen wir uns einen bestehenden Test an, der verdeutlicht, wie Cypress Komponenten für den Test bereitstellt. Dazu nehmen wir die Komponente, die einen einzelnen Buchungseintrag mit allen seinen Daten sowie die zum Buchungseintrag gehörenden Buttons darstellt und Events auslöst, wenn einer der Buttons gedrückt wird. In Listing 1 findet sich der dazugehörende Testfall.
Listing 1: Testfall für den Löschen-Button in der Komponente für einen Buchungseintrag
Die zu testende Komponente wird durch den Befehl cy.mount initialisiert, die anzuzeigende Buchung wird direkt an die Komponente übergeben. Anschließend überprüft der Test, ob ein Klick auf den Löschen-Button das entsprechende Delete-Event auslöst und mit der Id der zu löschenden Buchung weiterleitet. Abbildung 4 zeigt den gleichen Testfall, wie er in der Cypress-Testumgebung ausgeführt wird.
Abb. 4: Test des Löschen-Buttons in der Komponente für einen Buchungseintrag in der Cypress-Testumgebung
In der Testumgebung wird beim Hovern über einen Testschritt der entsprechende DOM-Snapshot angezeigt und erkannte Elemente (hier: der Löschen-Button) werden hervorgehoben.
Um einen Buchungseintrag bearbeiten zu können, brauchen wir zum einen ein entsprechendes Eingabeformular und zum anderen einen Button, mit dem sich dieses Formular aufrufen lässt.
Starten wir zunächst mit dem Button in der Komponente, die wir bereits entwickelt haben. Wir ergänzen die Tests für diese um einen weiteren Test, der fordert, dass es einen Bearbeiten-Button geben muss. Die immer noch offene Cypress-Testumgebung führt den zusätzlichen Test mit den anderen Tests für diese Komponente automatisch aus und der Test wird – wie bei testgetriebener Entwicklung üblich – fehlschlagen. Wir ergänzen also den Button und der Test wird grün. Erst in einem zweiten Schritt prüfen wir, ob bei einem Klick auf den neu hinzugefügten Button das Event zum Bearbeiten mit der richtigen ID ausgelöst wird. Auch dieser Test wird zunächst fehlschlagen, wie in Abbildung 5 gezeigt.
Abb. 5: Fehlschlagender Test des Bearbeiten-Buttons in der Komponente für einen Buchungseintrag in der Cypress-Testumgebung
Wir sehen, dass der Button geklickt, aber das erwartete Event nicht ausgelöst wurde. Auch diesen Testfall fixen wir anschließend durch die Integration des erwarteten Events im Produktivcode.
Um den Buchungseintrag bearbeiten zu können, brauchen wir eine Formularkomponente. Auch diese entwickeln wir testgetrieben.
Dafür legen wir zunächst eine Testdatei an, hier: BookingItemForm.spec.tsx (siehe Listing 2).
Listing 2: Tests für das Buchungsformular
Im ersten Schritt mounten wir nur die neu zu erstellende Komponente BookingItemForm in mountBooking Form. Das zwingt uns, die Komponente anzulegen, ohne diese bereits mit Funktionalität zu versehen. Zudem erleichtert das Auslagern des Setups in eine eigene Funktion, wie wir es hier sehen, alle weiteren Tests. Wir nutzen diese Art des Setups häufig, um Komponenten in verschiedenen Zuständen und mit verschiedenen anzuzeigenden Daten zu initialisieren, indem wir entsprechende Parameter übergeben. Über Rückgaben oder besser Aliasse lassen sich Informationen des Setups im Test verwenden.
Im ersten Testfall erwarten wir die Eingabefelder, die wir für die Buchung benötigen. Ist der Produktivcode hierfür erstellt, kümmern wir uns im zweiten Testfall um die Anzeige passender Fehlermeldungen, zum Beispiel dass der Nutzer auf Pflichtfelder hingewiesen wird. Schließlich ergänzen wir einen dritten Testfall, um zu überprüfen, dass beim Klick auf den Submit-Button ein Buchungseintrag erzeugt und per Event übermittelt wird. Abbildung 6 zeigt den Stand der Komponentenentwicklung vor der Implementierung der vom dritten Testfall geforderten Funktion.
Abb. 6: Tests für das Buchungsformular in der Cypress-Testumgebung
Auf diese Weise kann das System Schritt für Schritt unter visueller Kontrolle aufgebaut werden. Um das Buchungsformular komplett zu integrieren, fehlt noch die Anbindung in der umgebenden Listenkomponente, die Einträge anzeigt und somit auch neue Einträge in der Liste einfügen müsste. Auch die Anbindung an den Server ist im Beispiel noch nicht umgesetzt.
Migration bestehender Testsuiten
Das Projekt, in das bei uns Cypress Component Testing als erstes Einzug gehalten hat, nutzte zu diesem Zeitpunkt bereits Cypress für die End-to-End-Tests, aber Jest [Jest] für die unteren Teststufen. Die lang laufenden, sehr instabilen Jest-Tests und die Verfügbarkeit von Cypress Component Testing in einer ersten Version waren für uns ein Grund, zunächst einige wenige Komponententests zu migrieren und dann schrittweise (fast) die gesamte Testsuite umzustellen.
Dabei sind wir nach dem Pfadfinderprinzip vorgegangen und haben zunächst Tests auf Cypress umgestellt, die besonders unzuverlässig (flaky) waren, dann alle, die aufgrund von Weiterentwicklung ergänzt oder angepasst werden mussten. Alle neuen Tests wurden nach der erfolgreichen Einführung direkt mit Cypress umgesetzt. Neben der höheren Stabilität gibt Cypress dem Entwickler bereits während des Zyklus aus Test – Code – Refactor – Test ein visuelles Feedback, wie die Komponente aussieht und sich verhält, was bei den Jest-Tests fehlt. Nach der Einführung von Cypress wollten die Entwickler dieses visuelle Feedback nicht mehr missen.
Um den Migrationsaufwand gering zu halten, empfiehlt es sich, auf Testwerkzeuge zu setzen, die eine breite Unterstützung von Bibliotheken für Testanfragen und Test-Assertions bieten. So lässt sich in beiden Testausführungsumgebungen – Jest und Cypress – die Testing Library [TestingLib] nutzen, mit der UI-Elemente auf unterschiedliche, DOM-unabhängige Weise identifiziert werden können, zum Beispiel anhand eines Labels (siehe auch Testfälle oben, in denen wir die Testing Library nutzen). Auch Chai [Chai] für Assertions im TDD/BDD-Stil kann in Kombination mit verschiedenen Testausführungsumgebungen genutzt werden. Die Migration von Testsuiten ist dann aus unserer Erfahrung zu einem überwiegenden Anteil reine Übersetzungsarbeit und damit kostengünstig und einfach möglich. Zudem erhöht sich die Lesbarkeit der Tests deutlich.
Ein Versuch eines Ausblicks
Die Welt der Web-Frontend-Entwicklung ändert sich sehr schnell. Werkzeuge, die heute als Stand der Technik gelten, sind morgen bereits veraltet. Neue Frameworks erscheinen, bestehende Frameworks entwickeln sich schnell weiter, sodass auch die Testwerkzeuge Schritt halten müssen. Unserer Erfahrung nach sind Web-Frontend-Entwickler sich der Schnelllebigkeit bewusst. Änderungen und Anpassungen sind meist eingeplant und eingepreist. Das bedeutet aber auch, dass neue Werkzeuge oft sehr schnell von der Community angenommen werden und Verbreitung finden.
Im Bereich der End2End-Tests hat Selenium [Sel] in den letzten Jahren wieder aufgeholt und mit Playwright [PlayW] und WebdriverIO [WebD] sind gleich zwei neue Player auf dem Markt erschienen, die Cypress mit ähnlichen Konzepten Konkurrenz machen wollen. Auch wenn Cypress mit den Component Tests aktuell punkten kann, ist nicht absehbar, ob dies auch langfristig so bleibt. Die kommende Entwicklung bleibt spannend.
Weitere Informationen
[BrSo21] R. Brill, D. Sokenou, Test von Webanwendungen – auf das Werkzeug kommt es an, in: Informatik Aktuell, 23.2.2021,
www.informatik-aktuell.de/entwicklung/methoden/test-von-webanwendungen-auf-das-werkzeug-kommt-es-an.html
[Chai] chaijs.com/
[Cyp] www.cypress.io/
[Jest] jestjs.io/
[npm] https://npmtrends.com/cypress-vs-playwright-vs-puppeteer
[PlayW] playwright.dev/
[Sel] www.selenium.dev/
[TestingLib] testing-library.com/
[WebD] webdriver.io/
1) Das Beispiel kann bei Interesse dem Leser zugänglich gemacht werden, E-Mail an die Autorin genügt.