adesso Blog

Unit Tests mit Jest schreiben – ist das Alltag für euch? Wenn ihr bereits Jest-Kennerinnen und -Kenner seid, verwendet ihr ganz sicher routiniert alle Basisfunktionen, aber wann habt ihr euch das letzte Mal wirklich Zeit genommen, um Jest in vollem Umfang zu erkunden? Vielleicht haben wir für euch noch ein unbekanntes Feature parat, das euch den Arbeitsalltag erleichtert.

Vielleicht kennt ihr Jest auch noch gar nicht? Kein Problem! Wir zeigen euch, was dieses Testing Framework unter anderem mitbringt, und möglicherweise findet ihr das ein oder andere nützliche Feature, das euch einen Grund liefert, Jest in Zukunft auszuprobieren.

Egal ob Jest-Profi oder interessierte Jest-Anfängerin beziehungsweise interessierter Jest-Anfänger, hier die Basics:

Jest ist ein JavaScript Testing Framework von Facebook, das viele Sprachen und Frameworks unterstützt, darunter TypeScript, Node.js, React, Angular und Vue. Laut State of JavaScript (https://2022.stateofjs.com/en-US/libraries/testing/) hält sich Jest bereits seit mindestens 2017 in den Top 5 beim Ranking der Testing Tools für JavaScript in Bezug auf Beibehaltung, Interesse, Nutzung und Bekanntheitsgrad.

In einigen Build Tools wie Nx ist das Jest Setup bereits im üblichen Projekt-Setup enthalten. Jest ist ein All-in-one-Paket, da sowohl ein Test Runner inkludiert ist, der für die Testausführung zuständig ist, als auch eine umfangreiche Assertion Library, um Erwartungen zu formulieren, sowie eine Mocking Library und vieles mehr.

Hier sind unsere Top 5 der Jest Features:

  • Der Coverage Report
  • Das Watch Plugin
  • Node Modules mocken
  • Timer Mocks
  • Snapshot Testing

Der Coverage Report

In eurem Team wurde ein Quality Gate definiert oder ihr wollt einfach mal wissen, wo ihr in der Testabdeckung aktuell steht? Der Jest Coverage Report gibt die Möglichkeit, ganz detailliert einiges über die Testabdeckung zu erfahren. Um euch einen solchen Coverage Report in der Konsole anzeigen zu lassen, könnt ihr beim Ausführen des Tests ganz einfach die –coverage Flag verwenden. Nun wird eine Tabelle mit Informationen zur Abdeckung in der Konsole dargestellt. Das Ganze sieht dann so aus:

Coverage ist nicht gleich Coverage. Wer mit Testtheorie noch nicht viel zu tun hatte, hier eine kurze Zusammenfassung: Während bei einer 100-prozentigen Anweisungsüberdeckung durch Anweisungsüberdeckungstest (Statement Coverage) jede Anweisung mindestens einmal ausgeführt wurde, heißt das nicht gleich, dass auch alle Konditionen überprüft wurden. Hier spricht man von einer Überdeckung durch Zweigüberdeckungstest (Branch Coverage). Ein weiteres Überdeckungsmaß wäre unter anderem die Zeilenüberdeckung, die unabhängig vom Kontrollflussgraph die Zeilenüberdeckung aller ausführbaren Quellcodezeilen angibt. In der Tabelle sehen wir nun genau diese Überdeckungsmaße und ihre Auswertung individuell und pro Datei aufgelistet.

% Stmts: Prozentualer Anteil aller Anweisungen, die mindestens einmal durch Tests ausgeführt wurden.

% Branch: Prozentualer Anteil aller Verzweigungen, deren Konditionen mindestens einmal durch Tests erfüllt und damit durchlaufen wurden.

% Funcs: Prozentualer Anteil aller Funktionen, die mindestens einmal durch Tests aufgerufen wurden.

% Lines: Prozentualer Anteil aller Quellcodezeilen, die mindestens einmal durch Tests ausgeführt wurden.

Die Tabelle kann teilweise etwas kontraintuitiv und sperrig wirken. Das ist aber gar kein Problem, denn auch hierfür gibt es eine Lösung: Mit --coverage --coverageDirectory='coverage' wird ein visuell ansprechender Coverage Report in eurem selbstgewählten Coverage Directory erstellt – das heißt, der Name ‘coverage’ ist frei wählbar. Im entsprechenden Ordner befindet sich nun eine „index.html“-Datei, die ihr im Browser öffnen könnt.

In beiden Ansichten unterteilt Jest farblich drei Wertungsmaße: Low (rot), Medium (gelb) und High (grün) Coverage. Mit einem Klick auf die entsprechende Datei bekommt ihr zudem genaue Informationen zur Abdeckung:

Ein Jest Coverage Report bietet euch tiefe Einblicke in die Testabdeckung und in die unterschiedlichen Überdeckungsmaße in eurem Projekt.

Das Watch Plugin

Jest bringt interessanterweise einen Watch Mode mit, der es ermöglicht, ein schnelles Feedback zu Codeänderungen zu bekommen. Ein Vorteil, denn häufig möchte man wissen, ob geänderter Code dazu führt, dass an anderer Stelle unerwünschte Nebeneffekte auftreten, die bestehende Funktionalität kaputt machen. Alle Tests erneut laufen zu lassen, ist allerdings sehr zeit- und rechenintensiv. Jest kann nun mit der CLI-Option --watch gestartet werden, um bei Dateiänderungen nur davon beeinflusste Tests erneut durchzuführen. Die etwas umfangreichere Variante - watchAll existiert ebenfalls und lässt alle Tests bei einer Änderung erneut laufen.

Doch es gibt noch weitere Interaktionen, die im Watch Mode möglich sind. Über verschiedene Keys können spezifische Aktionen ausgelöst werden.

  • 'f' lässt nur fehlgeschlagene Tests erneut laufen,
  • 'u' löst ein Update aller fehlgeschlagenen Snapshots aus und
  • 'i' startet einen interaktiven Modus, um Snapshots einzeln zu aktualisieren.

Doch bei den Built-in Keys ist noch nicht Schluss. Wer seine eigenen Watch Mode Prompts definieren möchte, kann ein eigenes Watch Plugin schreiben und sich in Jest Events einklinken. Dazu steht das folgende Watch Plugin Interface zum Implementieren zur Verfügung:

	
	export declare interface WatchPlugin {
	    isInternal?: boolean;
	    apply?: (hooks: JestHookSubscriber) => void;
	    getUsageInfo?: (globalConfig: Config.GlobalConfig) => UsageData | null;
	    onKey?: (value: string) => void;
	    run?: (
	       globalConfig: Config.GlobalConfig,
	       updateConfiqAndRun: UpdateConfigCallback,
	    ) => Promise<void | boolean>;
	}
	

Daraus sollten die Methoden apply, getUsageInfo und run in eurem eigenen Plugin definiert werden. Dieses wird dann nur noch unter jest.config.ts als Module exportiert und steht schon zur Verfügung.

Über die Implementierung der Methode apply() besteht Zugriff auf folgende Teile eines Testlebenszyklus: shouldRunTestSuite(testSuiteInfo), onTestRunComplete(results) und onFileChange({projects}).

Des Weiteren können Keys hinzugefügt oder per Default bestehende überschrieben werden, indem getUsageInfo() implementiert wird. Darin müssen ein Key und ein Prompt zurückgegeben werden und schon gibt es eine weitere Option im Watch Mode.

Um dann etwas auszuführen, wird run() implementiert. Wird der Key gedrückt, bekommt das Plugin die Kontrolle und kann den zweiten Parameter updateConfigAndRun nutzen, um einen Test Run zu triggern. Änderbare Parameter für diesen Test Run sind unter anderem collectCoverage, onlyFailures, testNamePattern, testPathPattern oder updateSnapshot. Anschließend wird die Kontrolle an Jest zurückgegeben.

Möchtet ihr ein eigenes Plugin schreiben, schaut euch am besten den Guide in der Jest-Dokumentation dazu genauer an: https://jestjs.io/docs/watch-plugins.

Node Modules mocken

Insbesondere in Bezug auf Unit Tests wollen wir in einem übersichtlichen und geschlossenen Raum testen. Einflüsse von außen bringen ungewollte Varianzen in euren Test. Gleichzeitig kann ein Unit Test echten Schaden anrichten, wenn er nicht in einer abgesicherten Testumgebung ausgeführt wird. Das kann zum Löschen oder Verändern von Produktivdaten oder auch zum Senden echter Requests führen. Ein einheitliches und sicheres Mocking-Konzept erspart Entwicklerinnen und Entwicklern unangenehme und gefährliche Situationen. Grundsätzlich gilt im Zusammenhang mit Tests: Testet nichts, was ihr nicht ändern könnt. Deshalb kann es sehr sinnvoll sein, ganze Node Modules (etwa Lodash) direkt aus euren Tests zu verbannen und sie global zu stubben oder zu mocken.

In der Praxis legt man ein Unterverzeichnis mit dem Namen __mocks__/ direkt angrenzend an das Verzeichnis node_modules an. Achtung: Das Unterverzeichnis muss genau so heißen! Zudem ein besonderer Hinweis zu Built-in Node Modules: Diese werden nicht automatisch gemockt und müssen mit jest.mock(‘fs’) – das filesystem (fs) ist so ein Built-in Module – explizit aufgerufen werden. Das scheint erstmal kontraintuitiv, da Imports in Testdateien vor Mocks geschrieben werden. Aber was passiert nun, wenn ein Import ein eigentlich gemocktes Module nutzen möchte, bevor man überhaupt die Chance hatte, den Mock aufzurufen? Glücklicherweise unterstützt hier Jest Hoisting. Damit werden eure Aufrufe von jest.mock allen Imports vorangestellt, auch wenn das der Lesereihenfolge nicht direkt zu entnehmen ist.

In diesem Verzeichnis __mocks__/ könnt ihr nun alle Node Modules global mocken.

Ein Nachteil manueller Mocks ist natürlich, dass ihr sie gegebenenfalls anpassen müsst, wenn sich das originale Modul verändert. Mit jest.createMockFromModule wird daher ein automatischer Mock erzeugt und ihr könnt den Default an den Stellen überschreiben, wo es für euch relevant ist. Das ist auch die von Jest empfohlene Strategie.

Einige gute Beispiele und weitere Details findet ihr in der Jest-Dokumentation unter https://jestjs.io/docs/manual-mocks. Es lohnt sich definitiv, diese Option in Betracht zu ziehen und eine einheitliche Mocking-Strategie für Node Modules zu definieren, bevor eine Entwicklerin oder ein Entwickler im Projekt für sich und in jedem Test erneut und individuell Node Modules handhabt. Oder noch schlimmer: diese gar nicht stubbt oder mockt.

Timer Mocks

Ist bei euch auch schon ein Test fehlgeschlagen, weil irgendwo ein setTimeout() im Code war und Dinge, die ihr in eurem Test erwartet hattet, einfach noch nicht passiert waren? Entweder man mockt dann mühsam manuell Timer-Funktionen und macht sich Gedanken, ob der Test wirklich noch sinnvoll ist, da die Mock-Implementierung den Code sofort ausführt, oder man bedient sich der Timer Mocks von Jest. Zum Beispiel kann der einfache Aufruf von jest.useFakeTimers() jegliche Timer-Funktionen wie setTimeout(), setInterval(), clearTimeout(), clearInterval() usw. ersetzen. Da jest.useRealTimers() alle Timer wiederherstellt, können Timer-Funktionen innerhalb einer Testdatei, Testsuite oder sogar innerhalb eines Tests flexibel an- und abgeschaltet werden.

Ist es in manchen Fällen wichtig, dass der Test prüft, ob wirklich nach einer bestimmten Zeit erst ein Call ausgeführt wurde, kann die Zeit mit jest.runAllTimers() vorgespult werden, bis alle Timer ausgeführt wurden. Dadurch ist es möglich, direkt im Anschluss im Testcode zu prüfen, ob der Call erfolgt ist. Man kann allerdings auch noch spezifischer bestimmen, wie weit die Zeit vorgespult wird. Über jest.advanceTimersByTime(x) wird genau um x Millisekunden vorgespult. Alle Timer, die in dieser Zeit ausführbar sind, werden sofort ausgeführt. Außerdem kann gesteuert werden, dass bestimmte Timer explizit nicht gefakt werden sollen, indem dem zuvor genannten jest.useFakeTimers() eine Konfiguration mit den nicht zu fakenden Funktionen übergeben wird.

Doch Vorsicht, eine Falle gibt es: Enthält der Code rekursive Timer, führt jest.runAllTimers() zu einer Endlosschleife. Jest fängt die Schleife zwar irgendwann ab und wirft einen Fehler, jedoch sollte man den Code lieber vorab auf Rekursion prüfen, da erst nach 100.000 Timer-Ausführungen abgebrochen wird. Um das Problem zu umgehen, kann im Test stattdessen jest.runOnlyPendingTimers() aufgerufen werden.

Weitere Informationen zu Timer Mocks sind im Jest Guide unter https://jestjs.io/docs/timer-mocks und in der Fake-Timer-API-Dokumentation unter https://jestjs.io/docs/jest-object#fake-timers zu finden.

Snapshot Testing

Ein Snapshot ist im Wesentlichen nichts anderes als genau das, was das Wort schon sagt: ein Schnappschuss. Also das Abbild eines Objektes zu einem bestimmten Zeitpunkt, von dem wir in einem Test erwarten, dass es ein spezifisches Aussehen hat. Letztlich testen wir auch einen Snapshot in simplen Erwartungsdefinitionen wie expect(actualNumber).toBe(expectedNumber), denn man erwartet, dass die Variable actualNumber zu diesem Zeitpunkt den Wert expectedNumber hat. Diese Codezeile ist einfach zu formulieren und es ist leicht zu verstehen, was die Erwartungshaltung ist. Daher ist auch klar, was das Problem ist, wenn der Test failt. Also warum bietet Jest zusätzlich etwas namens Snapshot Testing an?

Interessant wird es, wenn die meistens verwendeten Matcher – wie das bereits erwähnte .toBe()) – an ihre Limits kommen. Das passiert, wenn wir komplette UI-Rendering-Ergebnisse erwarten. Im Grunde soll sichergestellt werden, dass nach einem gewissen Dateninput auf Ebene der HTML-Struktur ein erwartetes Ergebnis eintritt. Es wäre meist sehr zeitintensiv und würde zu schlecht lesbarem Code führen, manuell einen kompletten, vermutlich ewig langen String einzufügen, der erwartet wird. Muss dieser überarbeitet werden, ist es aufwendig, den sich ändernden Teil zu identifizieren und richtig anzupassen. Um solche Tests dennoch mit wenig Aufwand und in wartbarer Art und Weise durchführen zu können, gibt es bei Jest den Matcher .toMatchSnapshot(). Dessen Einsatz kann zum Beispiel so aussehen:

	
	it ( name: 'should render kpi card with difference', fn: () => {
	  kpiCardComponent.component.title = 'Besucher';
	  kpiCardComponent.component.value= 10000000;      
	  kpiCardComponent.component.difference= '10%';   
	  kpiCardComponent.detectChanges();
	  expect (kpiCardComponent.element).toMatchSnapshot ();
	});
	

Jest setzt einen Test Renderer ein, der beim ersten Testlauf das Snapshot-Ergebnis in einer Datei speichert. Dazu wird ein Ordner mit dem Namen __snapshots__ auf der Ebene der Testdatei angelegt. Darin wird eine Datei mit dem Namen der Testdatei plus der Endung .snap erstellt. Im Projektverzeichnis sieht das dann so aus:

Dabei handelt es sich um eine simple Textdatei, die pro Test die Beschreibung und den exportierten Vergleichswert enthält. Das kann zum Beispiel so aussehen:

Bei jedem weiteren Testlauf wird das erzeugte Ergebnis mit diesem verglichen, um festzustellen, ob es unerwünschte Änderungen gegeben hat. Der Testcode bleibt, wie oben zu sehen, mit der Zeile expect(actualHtml).toMatchSnapshot() schlank und es entsteht kein Aufwand beim Erzeugen des gewünschten Ergebnisses.

Schlägt der Test fehl, da der Vergleich negativ ausfällt, untermalt Jest in der Ausgabe die abweichende Code-Stelle. Treten erwünschte Änderungen auf und der Referenz-Snapshot muss erneuert werden, kann Jest mit dem Flag –updateSnapshot gestartet werden. Wichtig ist, dass die Snapshot Datei committet wird und Teil eures Code Review ist. Auch hierbei unterstützt Jest, da der Snapshot automatisch so formatiert wird, dass er für Menschen lesbarer ist.

Falls es nicht gewünscht ist, dass der zu erwartende Snapshot in einer separaten Datei abgelegt wird, gibt es eine weitere Alternative. Der Jest Matcher .toMatchInlineSnapshot() vergleicht ebenso einen Snapshot, allerdings wird bei der ersten Ausführung der Snapshot direkt in die Parameter-Klammer des Matchers geschrieben. Somit wird die Testdatei möglicherweise sehr lang, aber Ist- und Soll-Werte sind direkt hintereinanderweg zu lesen. So oder so ist es zu empfehlen, sich im Projekt auf einen Weg zu einigen, damit jeder den Snapshot schnell findet. In beiden Fällen sollten die Code-Reviewerinnen und -Reviewer darauf achten, dass der Snapshot in der Datei beziehungsweise inline generiert und committet wurde.

Weitere Informationen zu Snapshot Testing finden sich in der Jest-Dokumentation unter https://jestjs.io/docs/snapshot-testing.

Fazit

Sich für die richtigen Testing Tools zu entscheiden ist sicherlich keine einfache Angelegenheit. Statt sich einen eigenen Test Stack mit Mocking und Assertion Library, eventuellem Watch Plugin, einem Coverage Report Tool und einem Test Runner zusammenzustellen, bietet Jest das All-inclusive-Komfort-Paket. Dieser Blog-Beitrag soll keine Pro-und-Kontra-Einschätzung zu Jest als Test Framework geben. Wenn man sich allerdings dazu entschließt, auf ein externes Framework zu setzen, das von einem großen Konzern wie Facebook entwickelt und angeboten wird, sollte man auch sichergehen, alle Vorteile zu kennen und bei Bedarf für sich zu nutzen.

In unserem Blog-Beitrag haben wir daher unsere persönlichen Top 5 Jest Features aufgeführt, die ihr vielleicht noch gar nicht für euch entdeckt habt. Wir wünschen euch daher ganz viel Spaß beim Ausprobieren!

Ihr möchtet mehr über spannende Themen aus der adesso-Welt erfahren? Dann werft auch einen Blick in unsere bisher erschienenen Blog-Beiträge.

Bild Franziska Scheeben

Autorin Franziska Scheeben

Franziska Scheeben ist Softwareentwicklerin bei adesso im Bereich Health und Life Sciences. Sie ist Full-Stack-Entwicklerin mit Schwerpunkt auf Webanwendungen im Kontext von Angular und Spring. Um qualitativ hochwertige Software zu entwickeln, sind für sie umfangreiche und vor allem aussagekräftige Tests ein wesentlicher Grundbaustein.

Bild Milena Fluck

Autorin Milena Fluck

Milena Fluck ist seit 2020 Software Engineer bei adesso und verfügt über umfangreiche Projekterfahrung im Gesundheitswesen. Ihr aktueller Fokus liegt auf dem Einsatz von JavaScript und TypeScript in der Frontend- und Backend-Entwicklung. Sie bevorzugt Test Driven Development. Dabei dürfen aussagekräftige Unit-Tests natürlich nicht fehlen.

Diese Seite speichern. Diese Seite entfernen.