ts-jest, jest.config.ts und jest.mocked() praxisnah
Wer Jest ohne saubere TypeScript-Anbindung einsetzt, verliert die entscheidenden Vorteile von Typsicherheit genau dort, wo Fehler am teuersten sind, nämlich in Tests und Mocks. Dieser Artikel zeigt, wie sich ts-jest und babel-jest unterscheiden, wie jest.config.ts typsicher aufgesetzt wird und wie Mock-Funktionen, Modul-Mocks und Klassen-Mocks mit echten Typen statt any geschrieben werden.
Inhaltsverzeichnis
- 1. Warum Jest mit TypeScript kombinieren: ts-jest vs. babel-jest
- 2. Jest für TypeScript-Projekte konfigurieren
- 3. jest.config.ts typsicher aufsetzen
- 4. Mock-Funktionen typsicher: jest.Mock und jest.fn<T>
- 5. jest.mock() für ganze Module typisieren
- 6. Klassen und typisierte Klasseninstanzen mocken
- 7. Typische Stolpersteine: Partial Mocks und jest.mocked()
- 8. Praxisbeispiel: Async-Funktion und API-Call typsicher testen
- 9. Snapshot-Testing mit Typen und Vergleich: any vs. typsicher
- 10. Zusammenfassung
- 11. FAQ
1. Warum Jest mit TypeScript kombinieren: ts-jest vs. babel-jest
Jest ist der meistgenutzte Test-Runner im JavaScript-Ökosystem, versteht nativ aber nur JavaScript, keine TypeScript-Syntax. Für TypeScript-Projekte braucht es deshalb eine Transform-Schicht, die .ts-Dateien vor der Ausführung in JavaScript umwandelt. Dafür stehen zwei etablierte Wege zur Wahl: ts-jest, das den echten TypeScript-Compiler nutzt und dabei auch typprüft, und babel-jest mit @babel/preset-typescript, das Typannotationen lediglich entfernt, ohne sie zu validieren.
Der Unterschied ist mehr als technisches Detail: babel-jest transformiert jede Datei isoliert und extrem schnell, erkennt dabei aber keine Typfehler, etwa einen falsch typisierten Rückgabewert oder ein fehlendes Interface-Feld. ts-jest dagegen führt bei jedem Testlauf eine echte Typprüfung durch und fängt genau solche Fehler ab, kostet dafür spürbar mehr Zeit bei großen Testsuiten. Die pragmatische Lösung vieler Teams: babel-jest für schnelle Testläufe im Watch-Modus, kombiniert mit einem separaten tsc --noEmit-Schritt in der CI-Pipeline, der die Typprüfung übernimmt, ohne den lokalen Testlauf zu verlangsamen.
2. Jest für TypeScript-Projekte konfigurieren
Wird ts-jest verwendet, läuft die Konfiguration über den Preset-Eintrag preset: 'ts-jest', der Jest anweist, .ts- und .tsx-Dateien über den TypeScript-Compiler zu transformieren. Entscheidend ist dabei die Option isolatedModules: Standardmäßig baut ts-jest ein vollständiges TypeScript-Programm mit allen Cross-File-Typinformationen auf, was bei großen Projekten spürbar langsamer ist. Mit isolatedModules: true wird jede Datei einzeln kompiliert, ohne Kenntnis anderer Dateien, was Testläufe deutlich beschleunigt, aber bestimmte typübergreifende Prüfungen wie const enum oder re-exportierte Typen nicht mehr abfängt.
Zusätzlich lohnt sich eine eigene tsconfig.spec.json, die von der Basis-tsconfig.json erbt, aber Test-spezifische Anpassungen vornimmt: @types/jest im types-Array ergänzen, damit globale Funktionen wie describe und expect typisiert sind, sowie gegebenenfalls strenge Compiler-Flags wie noUnusedLocals lockern, die in Testdateien mit vielen Mock-Importen unnötig stören. So bleibt die Produktionskonfiguration streng, während Tests pragmatisch bleiben.
3. jest.config.ts typsicher aufsetzen
Statt jest.config.js oder jest.config.json zu pflegen, lohnt sich eine jest.config.ts, die den Config-Typ aus dem jest-Paket importiert. Der Editor validiert dann jedes Feld gegen die tatsächliche Jest-Konfigurationsschnittstelle, Tippfehler in Optionsnamen wie testEviroment statt testEnvironment werden sofort als Compile-Fehler sichtbar, statt erst zur Laufzeit stillschweigend ignoriert zu werden.
// jest.config.ts: typsichere Jest-Konfiguration mit Autovervollständigung
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { isolatedModules: true, tsconfig: 'tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
clearMocks: true,
};
export default config;
Das Feld clearMocks: true verdient besondere Aufmerksamkeit: Ohne diese Option bleibt der Zustand von jest.fn()-Mocks zwischen Tests erhalten, was zu schwer nachvollziehbaren Fehlern führt, wenn ein Mock aus einem früheren Test unbeabsichtigt in einen späteren Test durchsickert. In Kombination mit restoreMocks: true lassen sich sogar per jest.spyOn überschriebene Implementierungen automatisch zurücksetzen.
4. Mock-Funktionen typsicher: jest.Mock und jest.fn<T>
Ohne Generics erzeugt jest.fn() eine Funktion vom Typ jest.Mock<any, any>, was jede Typsicherheit an dieser Stelle faktisch aushebelt. Die explizite Form jest.Mock<ReturnType, Args> erlaubt es, sowohl Rückgabetyp als auch Argument-Tupel anzugeben, sodass falsch aufgerufene Mocks bereits beim Kompilieren auffallen. Seit neueren Jest-Versionen ist der idiomatischere Weg jest.fn<typeof originalFunction>(), das die komplette Signatur direkt von der echten Funktion übernimmt, statt sie manuell zu duplizieren.
import { fetchProductPrice } from './pricing';
// Explizit typisierte Mock-Funktion: Rückgabetyp Promise<number>, ein string-Argument
const mockFetchPrice: jest.Mock<Promise<number>, [string]> = jest.fn();
// Idiomatischer seit Jest 29: Signatur direkt von der Originalfunktion ableiten
const typedMock = jest.fn<typeof fetchProductPrice>();
typedMock.mockResolvedValueOnce(49.9);
async function useMock() {
const price = await typedMock('SKU-1234');
// price ist als number inferiert, kein any-Leck
return price.toFixed(2);
}
Der Vorteil zeigt sich vor allem bei mockResolvedValueOnce und mockReturnValueOnce: Wird versehentlich ein String statt einer Zahl übergeben, meldet der Compiler den Fehler sofort, statt dass er erst als fehlschlagende Assertion im Testlauf auffällt. Gerade in größeren Teams verhindert das, dass sich any-typisierte Mocks unbemerkt durch die gesamte Testsuite ziehen und echte Regressionsfähigkeit verlieren.
5. jest.mock() für ganze Module typisieren
jest.mock('./modul') ersetzt automatisch alle Exporte eines Moduls durch Mock-Funktionen, doch TypeScript erkennt diesen Laufzeit-Effekt nicht: Der importierte Bezeichner behält weiterhin den ursprünglichen, nicht gemockten Typ. Wird darauf dann mockResolvedValueOnce aufgerufen, erscheint häufig die Fehlermeldung "Cannot invoke an object which is possibly undefined" oder ein Hinweis, dass die Methode mockResolvedValueOnce auf dem Original-Typ nicht existiert, weil TypeScript den Import weiterhin als echte Klasse oder Funktion sieht, nicht als Jest-Mock.
import { ProductRepository } from './product-repository';
jest.mock('./product-repository');
// Ohne jest.mocked() sieht TypeScript weiterhin den Original-Typ,
// ein Aufruf von mockResolvedValueOnce führt sonst zu einem Typfehler
const MockedRepository = jest.mocked(ProductRepository, { shallow: false });
describe('ProductRepository mock', () => {
it('returns the mocked product list', async () => {
MockedRepository.prototype.findAll.mockResolvedValueOnce([
{ sku: 'MS-1', name: 'Test Product' },
]);
const repo = new ProductRepository();
const products = await repo.findAll();
expect(products).toHaveLength(1);
expect(MockedRepository.prototype.findAll).toHaveBeenCalledTimes(1);
});
});
Der Helper jest.mocked(), seit Jest 27 fest im Kern enthalten, löst genau dieses Problem: Er nimmt den originalen Import entgegen und gibt eine typsicher als Mock erkannte Version zurück, bei der alle Funktionen als jest.Mock typisiert sind und die zusätzlichen Mock-Methoden wie mockResolvedValueOnce oder mockReturnValue zur Verfügung stehen, ohne die Objektstruktur des Originals zu verlieren.
6. Klassen und typisierte Klasseninstanzen mocken
Das Mocken von Klassen ist komplexer als bei einfachen Funktionen, weil sowohl der Konstruktor als auch die Prototyp-Methoden korrekt typisiert bleiben müssen. Bei jest.mock('./repository') wird die gesamte Klasse automatisch gemockt, der Zugriff auf einzelne Methoden erfolgt dann über jest.mocked(ClassName).prototype.methodName, wie im vorherigen Codebeispiel gezeigt. Für manuell erstellte Mock-Objekte, etwa als Testdouble ohne echten Klassenimport, hilft der Utility-Typ jest.Mocked<T>: Er wandelt jede Methode eines Interfaces oder einer Klasse in ihren jest.Mock-Gegenpart um und stellt sicher, dass keine Methode vergessen wird.
Für Tests, die nur einen Bruchteil der Methoden einer Klasse benötigen, bietet sich eine typisierte Factory-Funktion an, die ein Partial<jest.Mocked<T>> zurückgibt und erst am Ende der Testdatei über einen expliziten Cast auf jest.Mocked<T> vervollständigt wird. Wichtig dabei: Der Cast sollte erst erfolgen, nachdem alle vom Test tatsächlich benötigten Methoden vorhanden sind, sonst verschleiert er fehlende Implementierungen genauso wie ein pauschaler as any-Cast, den er eigentlich vermeiden soll.
7. Typische Stolpersteine: Partial Mocks und jest.mocked()
Ein häufiger Stolperstein sind Partial Mocks: Nur ein Teil der Exporte eines Moduls soll gemockt werden, der Rest soll die echte Implementierung behalten. Das gelingt über eine Mock-Factory in Kombination mit jest.requireActual, wobei die korrekte Typisierung erfordert, den Rückgabetyp von jest.requireActual<typeof import('./modul')>('./modul') explizit mit den überschriebenen Funktionen zu verschneiden, statt den gesamten Rückgabewert unkommentiert als any durchzureichen.
Ein zweiter, oft übersehener Stolperstein ist der zweite Parameter von jest.mocked(value, options): { shallow: true } mockt nur die oberste Ebene eines Objekts, geeignet etwa für Modul-Namespaces wie axios, bei denen nur einzelne Methoden wie get oder post als Mocks erkannt werden müssen. { shallow: false }, der Standardwert, mockt rekursiv auch verschachtelte Objekte und Klassenmethoden. Wird die falsche Option gewählt, erscheinen Fehler wie "Property does not exist on type Mock", weil TypeScript verschachtelte Eigenschaften nicht als Mock erkennt.
8. Praxisbeispiel: Async-Funktion und API-Call typsicher testen
Ein realistisches Szenario aus der Praxis: Eine Funktion ruft über axios den Bestellstatus einer Magento-Order aus einer REST-API ab und muss sowohl den Erfolgs- als auch den Fehlerfall abdecken. Der Import wird per jest.mock('axios') automatisch gemockt, jest.mocked(axios, { shallow: true }) stellt sicher, dass die einzelnen HTTP-Methoden wie get als Jest-Mocks erkannt werden, ohne das gesamte Axios-Objekt tief zu mocken.
import axios from 'axios';
import { getOrderStatus } from './order-service';
jest.mock('axios');
const mockedAxios = jest.mocked(axios, { shallow: true });
describe('getOrderStatus', () => {
it('returns a typed order status from the API', async () => {
mockedAxios.get.mockResolvedValueOnce({
data: { orderId: '1001', status: 'shipped' },
});
const result = await getOrderStatus('1001');
expect(result.status).toBe('shipped');
expect(mockedAxios.get).toHaveBeenCalledWith('/orders/1001');
});
it('propagates a typed error on request failure', async () => {
mockedAxios.get.mockRejectedValueOnce(new Error('Network Error'));
await expect(getOrderStatus('1001')).rejects.toThrow('Network Error');
});
});
Entscheidend ist, dass mockedAxios.get weiterhin gegen die echte Signatur von axios.get geprüft wird: Eine falsch typisierte URL oder ein falscher Response-Shape fällt schon beim Kompilieren auf. Der zweite Testfall mit mockRejectedValueOnce deckt den Fehlerpfad ab, sodass beide Codepfade der Funktion, Erfolg und Fehler, mit vollständiger Typsicherheit getestet werden, ohne dass an irgendeiner Stelle auf any zurückgegriffen werden muss.
9. Snapshot-Testing mit Typen und Vergleich: any vs. typsicher
Auch Snapshot-Tests profitieren von Typsicherheit, wenn die Testdaten gegen das echte Interface erstellt werden, statt als lose typisiertes Objektliteral. Ändert sich die zugrunde liegende Schnittstelle, etwa weil ein Pflichtfeld hinzukommt, meldet der Compiler den Fehler sofort im Testcode, statt dass der Snapshot stillschweigend veraltete Daten festschreibt und ein Entwickler ihn ungeprüft mit --ci=false --updateSnapshot überschreibt.
import { formatInvoice } from './invoice-formatter';
import type { Invoice } from './types';
describe('formatInvoice snapshot', () => {
it('matches the typed invoice snapshot', () => {
const invoice: Invoice = {
id: 'INV-2026-001',
total: 149.9,
currency: 'EUR',
items: [{ sku: 'MS-1', qty: 2, price: 49.9 }],
};
// TypeScript erzwingt die Invoice-Struktur zur Compile-Zeit,
// der Snapshot prüft zur Laufzeit nur die serialisierte Ausgabe
expect(formatInvoice(invoice)).toMatchSnapshot();
});
});
Die folgende Übersicht fasst die wichtigsten Unterschiede zwischen ungetypten any-Mocks und ihren typsicheren Gegenstücken zusammen, wie sie in den vorherigen Abschnitten Schritt für Schritt aufgebaut wurden.
| Szenario | Ohne Typsicherheit (any) | Typsichere Variante | Effekt |
|---|---|---|---|
| Mock-Funktion erstellen | jest.fn() ohne Generics | jest.fn<typeof fn>() | Autovervollständigung, Fehler bei falschen Argumenten |
| Modul mocken | jest.mock('./x'), Import bleibt Original-Typ | jest.mocked(x) | Zugriff auf mockResolvedValueOnce ohne Typfehler |
| Rückgabewert eines Mocks | mockReturnValue('x' as any) | jest.Mock<number, []> | Rückgabetyp wird gegen die echte Signatur geprüft |
| Klassen-Mock | new (MockedClass as any)() | jest.mocked(MockedClass, shallow: false) | Methoden-Signaturen der Klasse bleiben erhalten |
| Partial Mock eines Moduls | jest.requireActual(...) as any | jest.requireActual<typeof import('./x')>('./x') | Vermeidet stille Laufzeitfehler bei fehlenden Exporten |
| API-Response-Mock | axios.get = jest.fn() (Type Error) | jest.mocked(axios).get.mockResolvedValueOnce(...) | Kompiliert nur mit korrektem Response-Shape |
In der Praxis zahlt sich der zusätzliche Aufwand für typsichere Mocks schnell aus: Fehler, die sonst erst als flackernde Testfehlschläge auffallen, werden bereits beim Speichern der Datei im Editor sichtbar. Gerade in größeren Codebasen mit vielen Modul- und Klassen-Mocks verhindert das, dass sich any unbemerkt durch die gesamte Testsuite zieht und die eigentliche Absicherung der Typsicherheit im Produktivcode untergräbt.
Mironsoft
TypeScript-Testing, Jest-Setup und typsichere Mocks für Magento- und Headless-Projekte
Jest-Setup mit echter Typsicherheit?
Wir richten Jest und TypeScript für euer Projekt ein, migrieren bestehende any-Mocks auf typsichere Varianten und bauen belastbare Testsuiten für Frontend-Tooling, Build-Skripte und Headless-Integrationen.
Jest-Konfiguration
ts-jest oder babel-jest passend zur Projektgröße einrichten
Typsichere Mocks
any-Mocks durch jest.Mock, jest.fn<T> und jest.mocked() ersetzen
CI-Integration
Testsuiten und Type-Checks sauber in die CI/CD-Pipeline einbinden
10. Zusammenfassung
Jest mit TypeScript lohnt sich nur dann vollständig, wenn nicht nur der Produktivcode, sondern auch Mocks und Testdaten sauber typisiert sind. Die Wahl zwischen ts-jest und babel-jest ist eine Abwägung zwischen echter Typprüfung und Transform-Geschwindigkeit, die sich mit isolatedModules und einem separaten tsc --noEmit-Schritt in der CI oft gut kombinieren lässt. Eine typsichere jest.config.ts verhindert Konfigurationsfehler bereits beim Speichern, und jest.fn<typeof fn>() sowie jest.Mock<ReturnType, Args> sorgen dafür, dass Mock-Funktionen dieselbe Signatur wie das Original einhalten.
Der größte praktische Hebel liegt in jest.mocked(): Der Helper löst das häufige Problem, dass TypeScript nach jest.mock() weiterhin den Original-Typ eines Imports sieht, und macht Fehler wie "Cannot invoke an object which is possibly undefined" überflüssig. In Kombination mit typisierten Klassen-Mocks, Partial Mocks über jest.requireActual und typisierten Snapshot-Fixtures entsteht eine Testsuite, die Regressionen zuverlässig erkennt, statt nur Syntaxfehler stillschweigend zu übertünchen.
Jest mit TypeScript, Das Wichtigste auf einen Blick
ts-jest vs. babel-jest
ts-jest prüft Typen, ist aber langsamer. babel-jest transformiert nur, dafür schneller. isolatedModules als Mittelweg.
jest.config.ts
Der Config-Typ aus jest sorgt für validierte Konfigurationsfelder statt stiller Tippfehler.
jest.fn<T> und jest.Mock
Generics erzwingen korrekte Rückgabewerte und Argumente statt any-typisierter Mock-Funktionen.
jest.mocked()
Löst "Cannot invoke an object which is possibly undefined" bei Modul- und Klassen-Mocks zuverlässig.