End-2-End-Tests mit Playwright-Test
Wenn es um das automatisierte Testen von Webapplikationen geht, gibt es zwei grundlegende Ansätze: Möglichst fokussiert einzelne Teile (Units/Komponenten) oder möglichst viel auf einmal (End-2-End) testen. Beide Ansätze haben ihre Vor- und Nachteile und ihre Daseinsberechtigung. In diesem Artikel möchte ich ein Tool vorstellen, das – recht überzeugend, soviel sei bereits verraten – versucht, die größten Nachteile von E2E-Tests zu adressieren: Playwright-Test.
Unit-Tests vs. E2E-Tests
Vorteile Unit-Tests
Unit-Tests haben üblicherweise den Vorteil, dass sie relativ schnell ausgeführt werden können, da sie nur einen kleinen Teil der Anwendung auf einmal testen. Alle kritischen Teile, wie z. B. Netzwerk-Requests oder andere Komponenten in einem bestimmten Zustand werden gemockt, wodurch die Tests immer von einem vordefinierten Zustand ausgehen können und dadurch sehr zuverlässig sind. Außerdem können Unit-Tests meist recht gut parallelisiert werden und erfordern keinen gestarteten Browser. Für das Testen von komplizierter Logik sind Unit-Tests daher ideal.
Nachteile Unit-Tests
Dafür ist das Debugging bei Unit-Tests nicht immer einfach, wenn z. B. komplexe React-Komponenten getestet werden sollen. Das Schreiben von komplexen Mocks kann recht zeitaufwändig sein und birgt immer die Gefahr, dass die Mocks von der Realität abweichen und der Test damit kein realistisches Szenario abdeckt. Die Gewissheit, dass das Zusammenspiel mit anderen Bestandteilen der Software (sowohl im Frontend als auch mit dem Backend) wie gewünscht funktioniert, bekommt man nicht, da mehr oder weniger viel nur simuliert ist. Um insbesondere diesen Kritikpunkt zu adressieren, gibt es noch die Mischform der Integrationstests, die aber je nach Auslegung trotzdem die Vor- und Nachteile von Unit- bzw. E2E-Tests haben.
Manche Testszenarien können in Tests mit simuliertem DOM (z. B. über JSDOM) nicht sinnvoll abgebildet werden, z. B. ob ein IntersectionObserver oder ein ResizeObserver sinnvoll eingebunden sind oder ob ein Element die gewünschte Position im Layout hat (z. B. indem man offsetTop verwendet). Natürlich kann man die entsprechenden Funktionen mocken, aber die Aussagekraft der Tests schwindet damit deutlich. Eine Lösung dafür könnte sein, die Tests nicht in einem simulierten sondern in einem realen Browser auszuführen. Derzeit ist das allerdings keine gängige Variante.
Vorteile E2E-Tests
Der große Vorteil von E2E-Tests ist, dass man ein sehr benutzernahes Testszenario in einem realen Browser hat und dadurch mit deutlich höherer Gewissheit davon ausgehen kann, dass die Software auch für den Kunden wie gewünscht funktioniert.
Ein oft unterschätzter Vorteil von E2E-Tests ist die Unterstützung beim Entwickeln neuer Features. Man möchte ein Feature für Neukunden des Shops entwickeln? Die typische Vorgehensweise wäre wohl, während der Entwicklung durch Auskommentieren/ Überschreiben von entscheidenden Code-Stellen genau dieses Szenario zu erzeugen, ohne für jeden Debugging-Durchlauf einen neuen Benutzer anlegen und dessen Konto per E-Mail bestätigen zu müssen. Einfacher geht es mit einem entsprechenden E2E-Test. Wenn dieser automatisiert einen Neukunden anlegt, kann man in dem E2E-Test einen Breakpoint setzen und das neue Feature sehr komfortabel unter realistischen Bedingungen entwickeln und im Browser debuggen.
Nachteile E2E-Tests
Es gibt aber natürlich auch Nachteile von E2E-Tests. Die wohl größten Kritikpunkte an E2E-Tests sind die Dauer sowohl beim Schreiben der Tests als auch bei deren Ausführung und die mangelnde Zuverlässigkeit. Genau diese 3 Kritikpunkte versucht Playwright-Test zu adressieren.
Was ist Playwright-Test?
Der Name verrät es schon: Playwright-Test basiert auf Playwright. Playwright löst dasselbe Problem wie das vielleicht etwas bekanntere Puppeteer: Man kann einen Browser fernsteuern. Das ist natürlich eine ideale Voraussetzung für E2E-Tests und auch die Grundlage von Playwright-Test, dem Testrunner von Playwright.
Playwright vs. Puppeteer
Bei Playwright stellt sich unweigerlich die Frage nach den Unterschieden zu Puppeteer. Beide verwenden das DevTools-Protokoll, um mit dem Browser zu interagieren und können Browser sichtbar und unsichtbar (headless) steuern, außerdem sind beide Projekte Open Source.
Der Fokus der Projekte ist allerdings unterschiedlich. Puppeteer wird vom Chrome DevTools-Team entwickelt [1] und hat als Ziel die Fernsteuerung des Chrome- bzw. Chromium-Browsers. Playwright kann man in gewisser Weise als Nachfolger mit Cross-Browser-Anspruch sehen. Die Software wird u. a. von ehemaligen Puppeteer-Entwicklern als Microsoft-Projekt entwickelt. An zahlreichen Stellen merkt man, dass die mit Puppeteer gewonnenen Erfahrungen in ausgereiftere und komfortablere APIs von Playwright geflossen sind.
Ein Kernfeature ist das Auto-Waiting [2]: Wenn man z. B. einen Button anklickt, überprüft Playwright-Test vorher automatisch, ob das Element im DOM vorhanden, sichtbar, nicht in einer Animation, erreichbar für Events und nicht deaktiviert ist. Oder anders formuliert: Ob ein realer Benutzer das Element auch anklicken könnte. Dabei überprüft Playwright-Test die impliziten Checks genauso wie manuelle Assertions automatisch bis zu einem definierten Maximal-Timeout. Neben dem Komfortgewinn ist ein großer Vorteil, dass man nicht auf Verdacht Timeouts für asynchrone Operation (wie z. B. einen HTTP-Request) einbauen muss, sondern nur exakt so lange warten muss, wie es unbedingt nötig ist. Das erhöht die Testausführungsgeschwindigkeit merklich.
Mit Playwright-Test können sogar Multi-Tab-, Multi-Origin- oder Multi-User-Setups getestet werden. Außerdem können neben den großen Desktop-Browsern Chrome, Firefox, Edge und Safari auch mobile Browser (Google Chrome for Android und iOS Safari) getestet werden. Safari lässt sich dabei auch auf Nicht-Apple-Betriebssystemen verwenden, was insbesondere in CI-Umgebungen wichtig ist.
Playwright vs. Cypress
Neben Puppeteer gibt es noch einen weiteres vergleichbares Produkt: Cypress. Ein wesentlicher Unterschied von Playwright-Test und Cypress ist, dass Playwright-Test den Browser von außen (out of process) steuert und nicht selbst im Browser läuft. Ein Klick bei Cypress ist beispielsweise der Aufruf der .click()-Methode auf dem entsprechenden Element, wohingegen Playwright-Test einen "richtigen" Klick durchführt. Das kann in bestimmten Edge Cases zu unterschiedlichen Ergebnissen führen.
Der Out-of-process-Ansatz von Playwright ermöglicht auch das bereits erwähnte Testen von Anwendungen in mehreren Tabs, mit mehreren Origins oder sogar mehreren Benutzern, was bei Cypress architekturbedingt nicht möglich ist [3].
Das Geschäftsmodell hinter Cypress ist das Verkaufen eines Dashboards für Cypress in CI-Umgebungen [4], das als Hauptvorteil eine starke Parallelisierung der Tests und damit eine schnellere Ausführgeschwindigkeit bietet. Playwright hat als Microsoft-Projekt ohne Gewinnerzielungsabsicht einen fast schon unfairen Vorteil und bietet umfangreiche Parallelisierungslösungen ohne Kosten/Limitierungen an, egal ob auf dem Entwickler-Rechner oder in der CI-Pipeline.
Vorteile von Playwright-Test
Tests können sehr bequem geschrieben werden
Der erste große Kritikpunkt von E2E-Tests ist das aufwändige Schreiben der Tests. Playwright-Test bietet hier eine tolle Funktion zum Aufzeichnen von Browser-Interaktionen: Man klickt auf den Record-Button und benutzt danach die Webseite wie gehabt, d. h. man klickt Links und Buttons an, gibt Text in Eingabefelder ein, scrollt zu den gewünschten Inhalten etc. Playwright-Test generiert dazu passende Selektoren und Assertions. Es empfiehlt sich, den generierten Code an der einen oder anderen Stelle anzupassen bzw. zu ergänzen, aber etwa 90 Prozent der Arbeit ist an der Stelle schon getan.
Wer mit den Assertions von Jest vertraut ist, wird sich sofort heimisch fühlen, denn Playwright-Test verwendet ebenfalls die expect-API von Jest [5], hat diese allerdings noch um einige zusätzliche Assertions für dynamische Webseiten erweitert. Und seit Version 1.27 gibt es sogar testing-library-ähnliche Selektoren [6].
Da Playwright und Playwright-Test in TypeScript geschrieben sind, hat die IDE bzw. der Editor sehr gute Autovervollständigungsoptionen anzubieten, die nach einer kurzen Eingewöhnungsphase in vielen Fällen den Blick in die Dokumentation überflüssig machen.
All-in-One-Test-Lösung
Auch wenn Playwright-Test klar auf das Testen von browserbasierten Anwendungen und insbesondere E2E-Tests ausgelegt ist, ist es nicht darauf beschränkt. Es gibt seit Version 1.22 (experimentellen) Support für Komponenten-Tests, aber auch API-Tests oder Unit-Tests können ausgeführt werden. Dafür lässt sich Playwright-Test übrigens auch ganz ohne Browser verwenden.
Manchmal möchte man einen Test explizit nur für einen bestimmten Browser wie z. B. Safari unter macOS schreiben, weil der Browser sich in einem bestimmten Szenario anders als die "Browser-Kollegen" verhält – kein Problem mit der test.skip()-Funktion von Playwright-Test [7].
Komfortables Debugging
Auch beim Debuggen von E2E-Tests kann Playwright-Test überzeugen: Man kann z. B. per Umgebungsvariable den Browser mit direkt geöffneten DevTools starten, was ideal ist, um fehlgeschlagene Netzwerk-Requests oder Skript-Fehler in der Console zu analysieren, die nur beim initialen Laden (also bevor man eine realistische Chance hat, die DevTools manuell zu öffnen) auftreten.
Wenn man an einer bestimmten Stelle im Test die Browser-Ausführung stoppen möchte, empfiehlt sich, die Zeile page.pause() in den Test einzubauen [8]. Dann kann man z. B. seine Selektoren über die (globale) Funktion playwright.$(selector) in der Browser-Console überprüfen, ohne früher oder später in ein Timeout-Problem zu laufen. Die Dokumentation enthält noch weitere nützliche Methoden des globalen playwright-Objekts [9].
Der Playwright Inspector ist ein nützliches Debugging-Tool, was sich in einem separaten Browser-Fenster öffnet und es ermöglicht, die Tests Schritt für Schritt zu durchlaufen, ähnlich wie im Debugger in der IDE/ dem Editor. Zum Öffnen muss man Playwright-Test mit dem --debug-Flag starten oder eine entsprechende Umgebungsvariable setzen [10].
Schlägt ein Test in der CI-Pipeline fehl, kann man sich bei entsprechender Konfiguration Videos, Screenshots und eine Trace-Datei des Testlaufs generieren lassen. Die Trace-Datei kann im eigens dafür entwickelten Trace Viewer angeschaut werden und enthält u. a. einen Netzwerk- und Konsolenlog und DOM-Snapshots für die einzelnen Test-Aktionen [11]. Damit ist es sehr komfortabel, den exakten Testdurchlauf nachzustellen und die Ursache des Fehlers zu finden und zu beheben.
Ähnlich wie z. B. bei Jest können mit test.only() oder test.skip() während des Debuggings Tests gezielt selektiert werden, damit nicht bei jedem Test-Lauf sämtliche Tests durchlaufen. Leider hat Playwright-Test derzeit keinen nativen Watch-Mode, es gibt allerdings Workarounds in dem dazugehörigen Issue bei Github [12].
Browser-Instanzen mit isolierten Browser-Kontexten
Playwright-Test kann in einer einzigen Browser-Instanz durch isolierte Browser-Kontexte mehrere Tests ausführen, ohne für jeden Test den Browser neu starten zu müssen. Ein Browser-Neustart würde jeden Test um mehr als 100ms verlängern, wohingegen das Erzeugen eines neuen Browser-Kontexts nur etwa eine Millisekunde dauert [13]. Dennoch sind die Tests dabei vollständig voneinander isoliert.
Schnelle Tests durch Parallelisierung
Je nach Anzahl der CPU-Kerne auf dem Hostsystem können die Tests mehr oder weniger parallelisiert werden, was die Ausführungsgeschwindigkeit stark erhöhen kann. Standardmäßig werden alle Tests in einer Datei im gleichen Worker-Prozess ausgeführt. Mit der Option fullyParallel kann das bei Bedarf angepasst werden [14]. Eine andere Stellschraube bietet die Anzahl der genutzten CPU-Kerne. In der Standardeinstellung nutzt Playwright-Test nur die Hälfte der zur Verfügung stehenden Kerne [15]. Erhöht man die Anzahl der genutzten Kerne auf 100 Prozent, laufen die Tests deutlich schneller auf dem lokalen Rechner, allerdings ist der Rechner während der Testausführung nur noch ein "Fernseher für E2E-Tests", also für das Entwickeln z. B. nicht mehr benutzbar. Je nach Ausstattung des eigenen Rechners können auch andere Werte sinnvoll sein. Außerdem ist es möglich, Tests auf mehrere Maschinen-Instanzen (Sharding) aufzuteilen, was insbesondere in einer CI-Pipeline nützlich ist [16].
Unzuverlässige Tests verhindern
Die Gründe, weshalb ein Test mal fehlschlägt und mal erfolgreich durchläuft, sind vielfältig und – soviel sei fairerweise gesagt – auch Playwright-Test kann dieses Problem nicht endgültig eliminieren. Allerdings bietet Playwright-Test auch hier nützliche Features: Findet ein Selektor mehr als ein passendes Element, wirft Playwright-Test einen Fehler und der Test bricht ab. Das eliminiert eine sehr gängige Fehlerquelle von E2E-Tests direkt.
Schlägt ein Test in der CI-Pipeline fehl, kann Playwright-Test den Test erneut ausführen (die genaue Anzahl Versuche ist konfigurierbar). Damit kann verhindert werden, dass z. B. temporäre Netzwerkprobleme zu fehlschlagenden Pipelines führen, was oft schon ausreicht, um doch einen erfolgreich durchlaufenden Test zu haben und Dank dem Trace Viewer ist es relativ einfach, die Ursache für einen unzuverlässigen Test herauszufinden.
Tipp: Escape Hatches für wiederkehrende Aufgaben
Ein typisches Problem von E2E-Tests ist, dass das Setup vor dem eigentlichen Test recht aufwendig ist. Bei vielen Webanwendungen muss man sich z.B. einloggen um die Anwendung benutzen zu können. Wenn man in jedem Test extra einen neuen Benutzer erstellen, die zugesandte E-Mail bestätigen (was alleine schon kompliziert genug ist) und sich dann auch noch einloggen muss, vergehen schnell mehrere Sekunden bevor der eigentliche Test ausgeführt werden kann. Eine mögliche Lösung ist es, einen Test zu schreiben, der den kompletten Flow zum Anlegen, Bestätigen der E-Mail und Einloggen des Benutzers abdeckt und für alle anderen Tests im Backend einen Escape-Hatch einzubauen, der mit einem einzigen Request den Benutzer anlegt, die E-Mail-Adresse bestätigt und den Benutzer einloggt. Den Escape-Hatch sollte man natürlich nicht auf der Produktivumgebung aktivieren, aber auf allen anderen Umgebungen kann man so von deutlich schneller ausgeführten Tests profitieren ohne das Risiko von übersehenen Fehlern einzugehen.
Tipp: E2E-Test-driven Development
Ein sinnvoller Ansatz ist auch, für jeden real aufgetretenen Bug einen entsprechenden E2E-Test zu schreiben. Wenn man den E2E-Test mit dem gewünschten Verhalten schreiben kann bevor der eigentliche Bug behoben ist, kann man sogar E2E-testgetrieben entwickeln und bekommt den passenden Regressionstest quasi frei Haus.
Weitere Informationen
Auch wenn es etwas seltsam anmuten mag, in einem Artikel über Playwright-Test auf den vielleicht größten Konkurrenten Cypress zu verweisen, sind die Cypress-Docs auch für Playwright-Test relevant und empfehlenswert, weil dort verschiedene Test-Strategien und -Ansätze beschrieben werden, die sich größtenteils auch auf Playwright-Test anwenden lassen [17].
Die Dokumentation von Playwright bzw. Playwright-Test unter playwright.dev ist ebenfalls eine gut geschriebene und umfangreiche Anlaufstelle für weitere Informationen [18]. Auf dem offiziellen YouTube-Kanal von Playwright erscheinen regelmäßig Videos zu den neuesten Versionen von Playwright, die nicht nur das Was sondern oft auch das Warum von Änderungen/Neuerungen kurzweilig und gut verständlich erklären [19].
- Puppeteer
- Auto-Waiting
- Cypress
- Dashboards für Cypress in CI-Umgebungen
- Jest
- testing-library-ähnliche Selektoren
- test.skip()-Funktion
- page.pause()-Funktion
- Methoden des globalen Playwright-Objekts
- Playwright Inspector
- Trace Viewer
- Issue bei Github
- Youtube: Introducing Playwright test runner
- Option fullyParallel
- testConfig.workers
- Parallelism and sharding
- Cypress-Docs
- Playwright
- YouTube-Kanal von Playwright