Jest mit TypeScript: Setup und typsichere Mocks
<T>
type
TypeScript · Jest · Unit Testing · Mocking
Jest mit TypeScript: Setup und typsichere Mocks
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.

13 Min. Lesezeit ts-jest · babel-jest · jest.mock() · jest.mocked() TypeScript 5 · Jest 29 · Node.js

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.

11. FAQ: Jest mit TypeScript

1Soll ich ts-jest oder babel-jest für TypeScript-Tests verwenden?
ts-jest prüft Typen, ist aber langsamer bei großen Suiten. babel-jest transformiert nur und ist schneller. Kombination mit tsc --noEmit in der CI ist üblich.
2Was macht die Option isolatedModules in ts-jest?
Kompiliert jede Datei einzeln ohne Kenntnis anderer Dateien, deutlich schneller, erkennt aber bestimmte typübergreifende Prüfungen nicht mehr.
3Warum sollte jest.config.ts statt jest.config.js verwendet werden?
Der Config-Typ aus jest validiert jedes Feld im Editor, Tippfehler fallen sofort als Compile-Fehler auf statt zur Laufzeit ignoriert zu werden.
4Wie type ich eine Mock-Funktion mit jest.fn korrekt?
Am besten mit jest.fn(), das die Signatur der echten Funktion übernimmt. Alternativ jest.Mock explizit angeben.
5Was bedeutet Cannot invoke an object which is possibly undefined bei jest.mock()?
TypeScript sieht nach jest.mock() weiterhin den Original-Typ des Imports. jest.mocked() wandelt den Import in eine typsicher erkannte Mock-Version um.
6Wie mocke ich eine ganze Klasse inklusive Prototyp-Methoden typsicher?
jest.mock('./klasse') plus jest.mocked(ClassName, { shallow: false }) für Klasse und Prototyp-Methoden. Für manuelle Objekte eignet sich jest.Mocked.
7Was ist ein Partial Mock und wie type ich ihn korrekt?
Mockt nur einzelne Exporte eines Moduls. Typsicher über jest.requireActual('./modul'), verschnitten mit den überschriebenen Funktionen.
8Wann verwende ich shallow: true bei jest.mocked()?
Für Modul-Namespaces wie axios mit einzelnen zu mockenden Methoden. shallow: false, der Standard, mockt rekursiv verschachtelte Objekte und Klassenmethoden.
9Wie teste ich eine asynchrone API-Funktion typsicher mit Jest?
Modul per jest.mock() mocken, mit jest.mocked() typsicher machen. mockResolvedValueOnce für den Erfolgsfall, mockRejectedValueOnce für den Fehlerfall.
10Profitieren Snapshot-Tests von TypeScript-Typisierung?
Ja. Testdaten gegen das echte Interface erstellen sorgt dafür, dass Schnittstellenänderungen sofort im Testcode auffallen statt in veralteten Snapshots.