Mocking mit Typescript und Jest

Mocking wird in Unit-Tests verwendet, um einzelne Komponenten leichter isoliert testen zu können. In Sprachen wie C# oder Java existieren dazu zahlreiche Bibliotheken, die es ermöglichen, mit nur wenigen Zeilen Code ein Mock-Objekt zu erstellen. Aber wie sieht das eigentlich in Typescript aus? In diesem Beitrag möchte ich anhand eines Beispiels untersuchen, wie man mit der Test-Bibliothek Jest eine Typescript-Klasse mithilfe von Mocks testen kann.

Das Beispiel

Um zu demonstrieren, welche Möglichkeiten es beim Mocken von Abhängigkeiten gibt, wird als Beispiel die folgende TodoService-Klasse verwendet. Diese hat eine Methode, die die Todos des angemeldeten Users zurückgibt und eine weitere Methode, die ein Todo als erledigt abspeichert. Um diese Aufgaben durchzuführen, greift die Klasse auf den TodoDataService und den UserService zu. Der UserService stellt den aktuell in der Anwendung angemeldeten User zur Verfügung, während der TodoDataService die Kommunikation mit dem Backend übernimmt und z.B. zum Abfragen aller Todos verwendet werden kann. Das komplette Beispiel-Projekt ist auf GitHub zu finden.

Abbildung 1 / Bildquelle: Christina Braun, generic.de AG

Um für die beiden Methoden Unit-Tests schreiben zu können, müssen die Abhängigkeiten gemockt werden. Wir wollen beeinflussen, welche Daten der TodoDataService und der UserService zurückgeben. Außerdem soll überprüft werden können, ob die updateTodo-Methode des TodoDataService auch wirklich aufgerufen wurde. Wichtig hierbei ist, dass die verwendeten Mock-Objekte typisiert sind. Wenn in Zukunft z.B. die getTodos-Methode umbenannt wird, sollte der Test nicht manuell angepasst werden müssen.

Erstellen von Mock-Objekten

Jests Mock-Functions bieten die Möglichkeit eine einzelne Methode zu mocken. Über den Aufruf jest.fn() wird eine Mock-Function erstellt. Anschließend kann z.B. mithilfe von mockReturnValue ein Rückgabewert definiert werden.

Abbildung 2 / Bildquelle: Christina Braun, generic.de AG

Man könnte sich daher ein Mock-Objekt für eine Klasse erstellen, indem man jede Methode durch eine Mock-Function ersetzt. Um den TodoDataService zu mocken, könnte man z.B. folgenden Code schreiben:

Abbildung 3 / Bildquelle: Christina Braun, generic.de AG

Dies hat jedoch einige Nachteile. Zum einen ist das Mock-Objekt nicht vom Typ TodoDataService, weil z.B. private Properties nicht enthalten sind. Um das Mock-Objekt an den TodoService zu übergeben, müsste daher zunächst eine Type-Assertion durchgeführt werden. Außerdem müssen alle zu mockenden Methoden nach dem gleichen Schema erstellt werden, sodass viele eigentlich überflüssige Zeilen Code geschrieben werden müssen.

Das npm-Package jest-create-mock-instance hat es sich zur Aufgabe gemacht, das Erstellen von Mocks für Klassen zu vereinfachen. Nach Installation des Packages kann ein Mock damit über die Methode createMockInstance erstellt werden. Somit ist nur noch eine Zeile Code zum Erstellen eines Mocks erforderlich.

Abbildung 4 / Bildquelle: Christina Braun, generic.de AG

Das Mock-Objekt für den TodoDataService ist dann vom Typ jest.Mocked<TodoDataService>. Da dieser Typ mit dem TodoDataService kompatibel ist, kann der Mock ohne eine Type-Assertion direkt an den TodoService übergeben werden. Die Typisierung als jest.Mocked bietet außerdem den Vorteil, dass Methoden wie mockReturnValue zur Verfügung stehen und während der Entwicklung durch die Code-Completion vorgeschlagen werden.

Mocken von Gettern

Für den UserService kann ebenfalls ein Mock über die createMockInstance-Methode erstellt werden. Im Gegensatz zum TodoDataService soll hier aber keine Methode sondern der Getter currentUser gemockt werden. Dies ist leider nicht über den Aufruf von mockReturnValue möglich. Da Jest hierfür keine Lösung bietet, habe ich mir folgende Hilfsfunktion geschrieben, die den Rückgabewert eines Getters mithilfe von Object.defineProperty definiert.

Abbildung 5 / Bildquelle: Christina Braun, generic.de AG

Der Rückgabewert von currentUser kann dann über den Aufruf der mockGetter-Methode bestimmt werden. Hierbei muss der Name der zu mockenden Property als String angebeben werden. Dies hat den Nachteil, dass der Aufruf bei Umbenennung der currentUser-Property nicht automatisch angepasst wird. Durch die Deklaration des Parameters als Key des Mock-Objekts würde uns im Fall einer Umbenennung aber zumindest ein Fehler angezeigt werden.

Abbildung 6 / Bildquelle: Christina Braun, generic.de AG

Verwendung der Mocks im Unit-Test

Beim Schreiben des Tests für den TodoService werden zunächst die Mocks für den TodoDataService und den UserService erstellt. Es ist wichtig, dass dies im beforeEach-Block erfolgt, da so jeder Test eigene Instanzen der Mocks verwendet. Dadurch wird vermieden, dass die Tests sich gegenseitig beeinflussen. Anschließend wird mithilfe der Mocks eine Instanz des TodoService erstellt.

Abbildung 7 / Bildquelle: Christina Braun, generic.de AG

Da die Mocks und die Instanz des zu testenden Services bereits im beforeEach-Block erstellt wurden, können die einzelne Tests direkt mit den erstellten Objekten arbeiten. Dabei wird i.d.R. zunächst definiert, welche Daten die Mocks zurückgeben sollen. Im Test für die getTodosCreatedByUser-Methode werden beispielsweise drei Todos und ein User erstellt, dem zwei der Todos zugeordnet sind. Anschließend wird geprüft, dass beim Aufruf der getTodosCreatedByUser-Methode nur die erwarteten Todos zurückgeben werden.

Abbildung 8 / Bildquelle: Christina Braun, generic.de AG

Beim Test der markAsDone-Methode wollen wir prüfen, ob die updateTodo-Methode des TodoDataService mit dem richtigen Parameter aufgerufen wurde. Trotzdem muss auch hier der Rückgabewert der updateTodo-Methode mit einem leeren Observable gemockt werden, da dieses Observable von der zu testenden Methode zurückgegeben wird und wir uns auf dieses subscriben müssen. Die Methode markAsDone wird anschließend mit einem noch nicht erledigten Todo aufgerufen. Mithilfe von Jests toHaveBeenCalledWith-Matcher kann dann bewiesen werden, dass ein Aufruf der updateTodo-Methode mit dem als erledigt markierten Todo-Objekt erfolgt ist.

Abbildung 9 / Bildquelle: Christina Braun, generic.de AG

Fazit

Jest kann nicht nur für die Ausführung von Unit-Tests verwendet werden, sondern stellt auch Mocking-Funktionalität zur Verfügung. Das Package jest-create-mock-instance baut auf dieser Funktionalität auf und erleichtert das Erstellen von Mock-Objekten einer Klasse. Anschließend können Rückgabewerte definiert und Methodenaufrufe überprüft werden. Lediglich zum Mocken von Gettern ist eine Hilfsfunktion erforderlich. Obwohl Jests Mocking-Funktionalität nicht speziell für Typescript gemacht ist, lässt sie sich auch in Typescript-Projekten ohne Einbußen bei der Typisierung verwenden.

Quellen

https://jestjs.io/docs/en/mock-functions

https://www.npmjs.com/package/jest-create-mock-instance

10.12.2018 von Christina, generic.de AG