Mock Dependencies in Unit-Tests mit TypeScript

In unserer Angular Anwendung KreditSmart nutzen wir zum Testen Karma, Jasmine angereichert mit Chai und Sinon. In vielen Komponenten- und Service-Tests versuchen wir die Abhängigkeiten zu mocken bzw. zu stubben. Das kann schon mal einige extra Zeilen Quellcode mit sich bringen. Beim Zugriff auf die Stubs möchten wir zudem gerne Codevervollständigung seitens der IDE oder des Editors beim Zugriff und Typsicherheit haben. Der Stub soll die gleichen Methoden wie der gemockte Service haben. Als Rückgabewert sollen die Methoden jedoch Sinon-Stubs liefern. Das ermöglicht uns in den Tests Rückgabewerte zu definieren oder Prüfungen auf Methodenaufrufe durchzuführen. Die Typen der Service-Feldern können erhalten bleiben.
Typisierung
Der Typ unseres Mocks ist Mock<T>
, wobei T
der zu mockende Service oder eine andere Klasse sein kann. Die TypeScript Deklaration sieht folgendermaßen aus:
GenericMethod
stellt eine beliebige Funktion dar. Gehen wir nun den ersten Teil der Typdeklaration von Mock<T>
durch.
Hier wird über alle Properties K
(also Felder und Methoden) des zu mockenden Services T
iteriert. Durch -readonly
wird die readonly
-Eigenschaft sämtlicher Properties entfernt und wir können auch diese Felder im Test setzen.
Wenn Property K
eine Methode (respektive Funktion) ist, so ist der Rückgabewert vom Typ SinonStub
. Andernfalls ist es ein Feld, dessen Typ aus dem Originalservice übernommen wird.
Jedoch wrappen wir den Typ noch in ein Partial. Somit können wir bei Bedarf auch Methoden des Feldes mocken. In unserer Anwendung haben wir etwa Felder vom Typ RxJS-Subject und wollen dessen next
-Methode für den Test stubben. Würden wir das Partial weglassen, müssten wir sämtliche Methoden von Subject stubben — das wollen wir nicht ;) .
Automatisiertes Mocken aller Methoden
Um automatisch sämtliche Methoden einer Klasse zu mocken, bedienen wir uns der Hilfsmethode mock<T>
:
Constructor<T>
ist, wie der Name schon sagt, eine Konstruktor-Funktion. Im TypeScript Umfeld ist es also unsere Klasse, die wir Mocken wollen. Dies liegt daran, dass JavaScript’s Klassenkonzept nicht Klassen-basiert ist wie in Java oder C++, sondern Prototypen-basiert. Mehr zum Vergleich der beiden Konzepte. Die Verwendung von Klassen in TypeScript und auch neueren JavaScript-Versionen ist syntaktischer Zucker.
Schauen wir uns die Deklaration der Methode mock<T>
genauer an:
mock<T>
erwartet also eine TypeScript-Klasse als Parameter und liefert einen Mock zurück. Ein Mock ist ein Objekt bei dem alle Methoden erhalten bleiben, jedoch als Rückgabewert einen SinonStub liefern. Die Felder bleiben erhalten. Bei der Verwendung der Methode ist es jedoch nicht notwendig mock<MyService>(MyService)
zu schreiben. Hier reicht mock(MyService)
aus, da TypeScript T
in der Funktion ableiten kann.
Die Implementierung iteriert daraufhin alle Properties der Klasse durch. Achtung — zur Laufzeit gehen Typinformationen verloren. Über den Prototyp eines Objektes können wir aber zumindest auf die Methoden des Objektes bzw. der Klasse zugreifen. Das reicht für unseren Fall aus, da die Implementierung der Klassen-Felder zur Laufzeit unverändert bleibt und wir nur die Typisierung zur Compile-Zeit brauchen.
Falls die obige Implementierung noch etwas funktionaler sein soll, lässt sich die mock
-Funktion auch folgendermaßen abändern:
updating
Anwendung
Wie sieht das ganze nun in der Anwendung aus? Bevor wir unsere Hilfskonstrukte hatten, sah das Setup in den Test folgendermaßen aus:
Mit unseren Konstrukten können wir das nun folgendermaßen schreiben:
Aufgrund von Type-Inference könnten wir sogar noch die Typdeklarationen der beiden Services weglassen:
Nicht nur ist der Code weniger verbose, wir erhalten auch noch gute Codevorschläge im jeweiligen Editor und mehr Typsicherheit.
Credits
Die obigen Konstrukte sind in Zusammenarbeit mit Robin Baum und Marc Redemske entstanden.