JavaScript-Code testen: Jasmine, Mocha und Jest im Vergleich

(Abbildung: Shutterstock / TippaPatt)
Nur bei sehr kurzen Projekten oder Prototypen verzichten Entwickler heute noch auf Tests. Ansonsten gehören sie – neben lesbarem Quellcode, einer soliden und modernen Architektur sowie einer verständliche Dokumentation – zum unverzichtbaren Qualitätsmerkmal guter JavaScript-Applikationen. Bei der Absicherung von Applikationen mit Unit Tests hat sich in den vergangenen Jahren viel getan. Im einfachsten Fall ist der Unit Test eine Funktion einer Applikation, die prüft, ob der zurückgegebene Wert den Erwartungen entspricht. Eine der simpelsten Lösungen in diesem Bereich ist das Assert-Modul von Node.js – ein Kernmodul der Plattform, mit dem Node.js selbst testet.

Die Architektur des Test-Runners Karma: Mit der Infrastrukturkomponente können Entwickler ihre JavaScript-Tests in einem oder mehreren Browsern durchführen. Sie ist notwendig, weil sich Test-Frameworks wie Jasmine oder Mocha auf die Tests konzentrieren und nicht auf die Infrastruktur. (Abbildungen: Karma, t3n)
Mit der bloßen Überprüfung von Werten ist es aber oft nicht getan. Moderne Test-Frameworks haben viele Komfortfunktionen für einfache Tests. Sie lassen sich anhand verschiedener Kriterien vergleichen. Zum Beispiel anhand der grundsätzlichen Syntax, den Überprüfungsfunktionen – häufig auch Matcher oder Assert-Funktionen genannt – oder der Architektur und dem Laufzeitverhalten. Jasmine war hier lange Zeit der Platzhirsch. Lösungen wie Mocha fristeten ein Nischendasein. Mittlerweile gibt es jedoch mehr Wettbewerb und jedes Framework hat seine Stärken und Schwächen.
Die Gemeinsamkeiten
Alle drei Frameworks – Jasmine, Mocha und Jest – sind in JavaScript geschrieben und werden als Open-Source-Projekt auf GitHub verwaltet. Sie lassen sich über einen Paketmanager – also NPM oder Yarn – im Projekt installieren. Sowohl Jasmine als auch Mocha und Jest sind unabhängige Frameworks. Das heißt, dass Entwickler damit client-seitige Applikationen im Browser und serverseitige Applikationen auf Node.js-Basis testen können.
Auch beim Aufbau der Tests gibt es viele Gemeinsamkeiten. In allen drei Frameworks lassen sich die Tests in sogenannten Test-Suites gruppieren. Pro Test-Suite gibt es die Möglichkeit, eigene Setup- und Teardown-Routinen zu definieren. Diese führen die Frameworks vor oder nach jedem Test aus. Außerdem können Entwickler Funktionen definieren, die zu Beginn und am Ende einer Test-Suite einmalig ablaufen. Die Test-Suites lassen sich in allen drei Frameworks mit der describe-Funktion erzeugen und ineinander verschachteln. So kann ein Entwicklerteam die Tests besser steuern und die Struktur sowie die Ausgabe der Ergebnisse aussagekräftiger gestalten. Die Tests werden mithilfe der it-Funktion geschrieben. Diese akzeptiert eine kurze Beschreibung (die vor allem für die Ausgabe der Testergebnisse zum Einsatz kommt) sowie eine Callback-Funktion (die den eigentlichen Test beinhaltet). Listing 1 zeigt ein Beispiel für einen einfachen Testfall.
describe('Calculator', () => { let calc; beforeEach(() => { calc = new Calc(); }); it('should add 1 and 1 and return 2', () => { const result = calc.add(1, 1); expect(result).toBe(2); }); it('should add 2 and 2 and return 4', () => { const result = calc.add(2, 2); expect(result).toBe(4); }); });
Beim grundsätzlichen Aufbau von Tests unterscheiden sich die Frameworks kaum. Alle folgen den Test-Patterns, die sich seit vielen Jahren etabliert haben. Beispiele hierfür sind die Organisation von Tests in Test-Suites, der Einsatz von Setup- und Teardown-Routinen oder die Möglichkeit Fixtures und Test-Doubles zu verwenden. Generell ist es wichtig, dass Entwickler die gleichen Qualitätsmaßstäbe für ihre Test-Codes anwenden wie für ihren produktiven Code – das wichtigste Kriterium ist also gute Lesbarkeit und Verständlichkeit.
Tests im Browser durchführen dank Karma
Frameworks wie Jasmine und Mocha konzentrieren sich auf die Implementierung von Tests und nicht auf die Infrastruktur. Gerade Frontend-Tests müssen jedoch im Browser laufen und an genau dieser Stelle kommt der Test-Runner Karma ins Spiel – eine Infrastrukturkomponente, mit der sich Browser fernsteuern lassen. Karma ist unabhängig vom Test-Framework. Installieren können Entwickler den Test-Runner über einen Paketmanager wie NPM. Danach konfigurieren sie Karma auf der Kommandozeile mit dem Befehl karma init. Ein interaktiver Assistent führt durch den Erstellungsprozess der Konfigurationsdatei, die standardmäßig den Namen karma.conf.js trägt. Diese Datei besteht aus JavaScript-Quellcode, der unter Node.js läuft. Entwickler können also auch eine einfache Logik einbauen. Karma hat eine Plugin-Infrastruktur, sodass Entwickler zum Beispiel das Paket „karma-jasmine“ installieren können, um Karma und Jasmine zu verbinden. Neben den Test-Frameworks ist Karma auch bei der Ansteuerung der Browser flexibel. So lassen sich über verschiedene Launcher-Pakete unterschiedliche Browser automatisch starten, sofern der Browser auf dem System installiert ist, auf dem Karma läuft. Andernfalls kann man einen Browser bei Karma remote registrieren. Häufig führen Entwickler Tests auf einem Continuous-Integration-Server aus, der meist über keine grafische Oberfläche verfügt. Dann können sie einen Headless Browser ohne grafische Ausgabe verwenden.
Jasmine, das Urgestein
Von den drei populären JavaScript-Frameworks ist Jasmine mit dem ältesten initialen Commit im Repository – nämlich vom November 2008. In der schnelllebigen JavaScript-Welt ist Jasmine damit ein Urgestein. Das allein ist jedoch noch kein Garant für Qualität. Weitere Qualitätsmerkmale sind die Downloadzahlen auf Npmjs.com, der Plattform hinter dem NPM-Package-Manager (Jasmine hat hier wöchentlich über zwei Millionen Downloads), die Bewertungssterne bei GitHub, das Verhältnis von offenen zu geschlossenen Issues, die Anzahl von Releases sowie das Datum des letzten Releases, also die Aktualität des Frameworks.
Wie schon erwähnt, lässt sich Jasmine sowohl client- als auch serverseitig einsetzen. Für serverseitige Tests müssen Entwickler nur das Paket „jasmine-core“ installieren. Für clientseitige Tests gibt es eine Stand-alone-Variante, die die Tests über ein Runner-Script in einer HTML-Datei ausführt. Viel häufiger kommt jedoch die Kombination von Karma und Jasmine zum Einsatz. Jasmine ist darüber hinaus nicht nur plattformunabhängig, es soll auch einen möglichst kleinen Kern behalten. Aus diesem Grund besitzt das jasmine-core-Paket auch keine externen Abhängigkeiten. Dennoch enthält Jasmine die wichtigsten Elemente, um Tests zu formulieren. Der Kern der Tests sind bei Jasmine die Matcher. Um eine Bedingung zu überprüfen, erhält die expect-Funktion den entsprechenden Wert. Anschließend kommt ein Matcher zur Anwendung, der prüft, ob die Bedingung erfüllt ist. Neben den üblichen Use Cases lassen sich auch Fehlerfälle und Exceptions testen. Außerdem kann Jasmine mit Asynchronität umgehen. Das Framework enthält bereits die wichtigsten Matcher. Reichen diese nicht aus, können Entwickler mit relativ geringem Aufwand weitere selbst definieren.
Die Test-Doubles von Jasmine gehen über diesen grundlegenden Funktionsumfang deutlich hinaus. Mit dem Spy-Feature lassen sich beispielsweise Funktionen überwachen und nahezu alle Aspekte inspizieren – etwa die Anzahl der Aufrufe, die Argumente oder Rückgabewerte. Noch einen Schritt weiter geht das Feature Stubs. Damit kann ein Entwickler das Verhalten von Funktionen definieren und so beispielsweise festlegen, dass eine bestimmte Funktion beim Aufruf einen vorbestimmten Wert zurückliefert. Stubs ersetzen so Abhängigkeiten zu Bibliotheken oder anderen Systemen, was die Stabilität und Laufzeit der Tests verbessert. In eine ähnliche Richtung geht das Clock-Feature von Jasmine für die Zeitkontrolle, Sprach-Features wie setTimeout, setInterval oder das Date-Objekt. Jasmine ist letztlich auch deshalb so verbreitet, weil große JavaScript-Frameworks wie Angular es standardmäßig mit ausliefern und es eine vorgefertigte Testumgebung bereitstellen.
Mocha, flexible Vielfalt
Während Jasmine im clientseitigen JavaScript weit verbreitet ist, kommt Mocha häufig in serverseitigen Applikationen mit Node.js zum Einsatz. Mocha ist etwas jünger als Jasmine. Sein erstes Commit stammt vom August 2011. In der Downloadstatistik liegt Mocha mit über 2,6 Millionen wöchentlichen Downloads vor Jasmine und auch bei GitHub hat Mocha mehr Sternchen vorzuweisen. Ein Grund dafür könnte sein, dass Mocha flexibler ist. Jasmine hält alles Nötige bereit – Mocha gibt Entwicklern eine wesentlich größere Freiheit. So lässt sich zum Beispiel die Assertion-Bibliothek austauschen – etwa durch Expect oder Chai. Beide Bibliotheken haben eine Syntax, die der von Jasmine ähnelt. Chai bietet jedoch neben der weitverbreiteten Expect-Schreibweise noch andere Formulierungen wie should oder assert.
Auch bei den erweiterten Features wie den Test-Doubles lässt sich Mocha mit externen Bibliotheken wie Sinon anwenden. Die übrigen Kern-Features wie Test-Suites, Setup- und Teardown-Routinen oder die Unterstützung asynchroner Tests enthält Mocha ebenso wie Jasmine. Auch die Umgebung für Mocha ähnelt der von Jasmine. Auf Node.js lässt sich Mocha nach der Installation des Frameworks und der erforderlichen Zusatzpakete direkt ausführen. Um Tests im Browser ablaufen zu lassen, können Entwickler auf eine Infrastrukturkomponente wie Karma zurückgreifen. Hierfür müssen sie nur den Karma-Mocha-Adapter installieren. Alle weiteren Karma-Plugins, wie etwa „Code-Coverage“, können für beide Frameworks verwendet werden.
Jest, die elegante Lösung
Der jüngste Vertreter der hier genannten JavaScript-Test-Frameworks ist das von Facebook entwickelte Jest mit einem initialen Commit vom Mai 2014. Jest ist auf React-Anwendungen spezialisiert, lässt sich aber – wie die anderen beiden Frameworks auch – unabhängig von JavaScript-Frameworks und -Bibliotheken einsetzen. So können Entwickler damit auch Angular-, Vue- oder Node.js-Applikationen testen. Mit Blick auf die Zahlen liegt der Newcomer klar vorn: Knapp vier Millionen wöchentliche Downloads und über 24.000 Sterne bei GitHub sprechen für sich. Einer der Gründe ist, dass die Entwickler aus den Schwächen anderer Frameworks gelernt haben. So ist ein großer Kritikpunkt an Jasmine, dass es die Tests sequenziell ausführt, was bei umfangreichen Anwendungen zu einer relativ langen Laufzeit führt. Jest führt die Tests hingegen parallel aus. Auch bei der Umgebung beschreitet Jest einen anderen Weg. Wo Mocha und Jasmine einen Browser zur Ausführung benötigen, nutzt Jest jsdom, das eine Browser-Umgebung simuliert. Auf diese Weise umgeht Jest das Problem, dass das Rendering im Browser die Performance verschlechtert.
Um Tests zu formulieren, können Entwickler die von Jasmine und Mocha bekannte Syntax nutzen. Bei Testfunktionen können sie neben der it- auch die test-Funktion verwenden. Beide unterscheiden sich jedoch in ihrer Funktionsweise nicht. Ein weiterer Vorteil von Jest ist, dass es im einfachsten Fall ganz ohne Konfiguration auskommt. Mit ein paar Handgriffen bietet es aber auch zusätzliche Features wie „Codecoverage“. Eine weitere interessante Funktion von Jest ist der sogenannte Snapshot-Test. Dabei zeichnet Jest die Struktur einer UI-Komponente auf und gleicht sie beim nächsten Testdurchlauf ab. Stimmt dabei etwas nicht überein, ist der Test fehlgeschlagen.
Fazit
Konkurrenz belebt bekanntlich das Geschäft und so können sich Entwickler über die Vielfalt der JavaScript-Test-Frameworks freuen. Vor allem, weil sie alle die gleichen Muster anwenden und eine ähnliche Syntax nutzen. Die Wahl hängt damit wohl vom jeweiligen Projekt und den Vorlieben des Entwicklers ab: Jasmine ist das Rundum-sorglos-Paket, mit dem sich die meisten Probleme auch ohne zusätzliche Komponenten lösen lassen. Mocha bietet zwar mehr Flexibilität, man muss sich aber auch mit den unterschiedlichen Möglichkeiten beschäftigen. Jest schließlich schlägt neue Wege ein und löst die typischsten Probleme beim Testen von Applikationen auf elegante Art.