Auf dem PrĂĽfstand: Wie JavaScript-Tests wieder lesbar werden
Es gibt viele Patterns, die helfen können, die Les- und Wartbarkeit von JavaScript-Tests zu verbessern. Zehn davon stellt dieser Artikel vor.
- Dr. Miriam Greis
Egal, bei welcher Gelegenheit: Bei Gesprächen über JavaScript und Frontend-Entwicklung dreht die Diskussion sich meist auch leidenschaftlich um das Testing. Der Einsatz passender Test-Frameworks wie beispielsweise Jest, der Testing Library oder spezifischer Bibliotheken (unter anderem Vue Test Utils) erleichtert Entwicklern und Entwicklerinnen die Arbeit. Dennoch ist es immer wieder eine Herausforderung, einfach lesbare und gut wartbare Tests zu schreiben.
Gerade in größeren Projekten ist das jedoch wichtig, denn andernfalls können Tests zu einem lästigen Übel werden, da ihre Wartung oder Anpassung bei Refactorings zunehmend Zeit verschlingt. Probleme verursachen insbesondere Tests, die zu stark an die Implementierung gekoppelt sind, komplizierte, mehrfach umbrechende Zeilen, duplizierter Code oder ungenaue Selektoren.
Zehn Patterns fĂĽr bessere Les- und Wartbarkeit
Dieser Artikel beschreibt zehn Patterns mit konkreter Umsetzung, die dabei helfen können, die Les- und Wartbarkeit von JavaScript-Tests zu erhöhen. Denn lesbare und leichter wartbare Tests verringern nicht nur zeitlichen Aufwand, sie wirken sich auch positiv auf die Codequalität aus und erhöhen die Motivation für das Schreiben von Tests. Die meisten der beschriebenen Muster sind vom verwendeten Frontend- oder Test-Framework unabhängig und auf andere Frameworks übertragbar. Die Praxisbeispiele basieren auf einem mehrere Jahre alten Vue-Projekt der Autorin mit über 1000 Frontend-Tests.
Zum Einsatz kommen Jest und die Vue Test Utils sowie zusätzlich Nightwatch.js für Tests im Browser direkt auf der Produktionsumgebung (was jedoch nicht Thema dieses Artikels ist). Das Projekt besteht aus einem sechs- bis zehnköpfigen Entwicklungsteam ohne dedizierte Tester und Testerinnen und ohne eine Aufteilung nach Spezialisierungen, sodass das gesamte Team auch für Backend-Services, CI/CD (Continuous Integration/Continuous Delivery) und Teile der Infrastruktur verantwortlich ist.
Die beschriebenen Vorschläge sind zwar stark vom Projektkontext wie dem Alter und Umfang oder den Anforderungen sowie Vorgaben abhängig und nicht unbedingt eins zu eins auf andere Projekte übertragbar. Trotzdem können sie unerfahrene Entwickler ebenso wie JavaScript-Expertinnen dazu anregen, Patterns zu evaluieren, auszuprobieren, zu verwerfen, zu kombinieren oder neu zu entdecken.
Mehr Integrationstests, weniger Unit-Tests
Typischerweise unterscheiden Entwickler und Entwicklerinnen Unit-Tests, Integrationstests und End-to-End-Tests. Unit-Tests prĂĽfen einzelne Teile von Anwendungen in Isolation, unter anderem einzelne Methoden oder Klassen, Integrationstests untersuchen das Zusammenspiel mehrerer Teile. End-to-End-Tests wiederum nehmen Anwendungen aus der Sicht der Benutzer und Benutzerinnen inklusive Backend und Datenbank unter die Lupe, teilweise sogar in der Produktionsumgebung. Dieser Artikel konzentriert sich vor allem auf Unit- und Integrationstests, End-to-End-Tests kommen nur am Rande vor, da sie ganz eigene Herausforderungen mit sich bringen und ihre Behandlung den Rahmen des Artikels sprengen wĂĽrde.
Die Testpyramide aus Mike Cohns Buch "Succeeding with Agile" (s. Abb. 1) ist allseits bekannt. Sie sieht als Basis eine große Zahl an Unit-Tests vor, weniger Integrationstests und noch weniger End-to-End-Tests, da die Fehleranfälligkeit, die Kosten für die Entwicklung und die Laufzeit mit der Art der Tests zunehmen.
1. Test-Doubles wie Mocks und Stubs gezielt einsetzen
Im Frontend-Bereich locken für Unit-Tests die von zahlreichen Test-Frameworks angebotenen "shallow"-Methoden als einfache Variante, um Komponenten in Isolation zu testen, gebräuchlich sind beispielsweise shallowMount aus den Vue Test Utils oder shallow von Enzyme. In Isolation bedeutet, dass der Test nur die einzelne Komponente rendert und Kind-Komponenten durch Test Doubles wie Mocks (Attrappen) oder Stubs (Stellvertreter) ersetzt werden. Da Entwickler und Entwicklerinnen damit testen, welche Daten zwischen den Komponenten hin- und hergehen, entsteht eine starke Kopplung an die Implementierung.
Bei größeren Refactorings führt das dazu, dass auch alle Tests nachzuziehen sind, wenn beispielsweise Komponenten, ihre Eigenschaften und CSS-Klassen umbenannt werden. Hier lohnt es sich, Mehraufwand in das Verständnis unterschiedlicher Methoden zu investieren, um den Einsatz von "shallow"-Methoden zu vermeiden. So lassen sich Test Doubles gezielt für Komponenten aus Bibliotheken und Plug-ins einsetzen, die im Test nicht angesprochen werden können. Die Notwendigkeit ihres Einsatzes für Kind-Komponenten entfällt.
Dieser Test ist an zwei Implementierungsdetails gekoppelt: an die Dialogkomponente und an ihre Property title
. Wird die Dialogkomponente nicht mitgerendert, kann sie nur ĂĽber ihre Schnittstellen getestet werden:
expect(wrapper.findComponent(Dialog).props().title).toBe('This is a title')
Der Test ist dagegen nicht an den Namen der Komponente oder der Property gekoppelt und testet stattdessen direkt, was Benutzer und Benutzerinnen später in der Anwendung sehen würden. Dafür ist die Dialogkomponente im Test mitzurendern:
expect(wrapper.text()).toContain('This is a title')
FĂĽr kleine Komponenten ohne eigene Logik, die nur Daten anzeigen (wie im Beispiel ein Dialog), entfallen somit Unit-Tests als Notwendigkeit, da sie in den Eltern-Komponenten mitgetestet werden. Deshalb wird die Testpyramide im Frontend-Bereich auch oft als Testing Trophy dargestellt, die mehr Integrationstests und weniger Unit-Tests vorsieht (s. Abb. 2). Neu hinzu kommen hier auch noch statische Tests, also beispielsweise statische Code-Analysen, die strukturelle Fehler bereits vor der Laufzeit erkennen.