Mocking in Unit Tests: Mock, Stub & Spy mit Jest

Veröffentlicht: 25. Mai 2026

Du schreibst Unit-Tests und merkst: jeder Test dauert 8 Sekunden. Die Test-Suite läuft 14 Minuten. Niemand führt sie lokal aus, der CI-Build wird beim Standup ignoriert. Die Ursache ist fast immer dieselbe. Deine Tests sprechen mit echten Datenbanken, echten APIs, echten Filesystems. Mocking ist die Technik, die das ändert.

In diesem Artikel zeige ich dir, was Mocks, Stubs und Spies wirklich unterscheiden, wie du sie in Jest sauber schreibst und welche Anti-Patterns Tests irgendwann unwartbar machen. Die Code-Beispiele sind in TypeScript, alle aus echten Projekten, in denen wir Suite-Laufzeiten von Minuten auf wenige Sekunden gedrückt haben.

Wir starten mit der ISTQB-konformen Definition, ordnen die fünf Test-Doubles (Mock, Stub, Spy, Fake, Dummy) ein, schreiben drei konkrete Jest-Beispiele und enden mit einem Entscheidungsbaum. Wenn du am Ende immer noch über die Jest-Syntax stolperst, hilft die FAQ.

Inhaltsverzeichnis

Was ist Mocking?

Das ISTQB-Glossar definiert einen Mock als "eine Art von Testdouble, das das erwartete Verhalten der Komponente während des Tests simuliert" (Quelle: glossary.istqb.org). Übersetzt heißt das: ein Mock ist ein Platzhalter-Objekt, das du im Test anstelle einer echten Abhängigkeit nutzt. Der Mock liefert vordefinierte Antworten und merkt sich, wie er aufgerufen wurde. Du verifizierst nach dem Test, ob die Aufrufe deinem erwarteten Verhalten entsprechen.

Die zweite, sehr alte Unterscheidung kommt von Martin Fowler in seinem Artikel zu Test Doubles: Behaviour Verification versus State Verification. Ein Stub prüft den Zustand nach Ausführung ("was steht jetzt in der Datenbank"), ein Mock prüft das Verhalten während der Ausführung ("welche Methoden wurden mit welchen Argumenten aufgerufen"). Diese Unterscheidung erklärt 80 Prozent der Verwirrung, die ich in Code-Reviews zu Test-Code sehe.

Mocking gehört zum technischen Handwerkszeug jedes Unit-Tests. Wenn du dich noch fragst, warum Unit-Tests überhaupt der erste Layer in der Test-Pyramide sind, lies parallel den Artikel Unit Tests: Sicherheitsnetz gegen KI-generierten Code als Grundlage.

Test-Doubles im Überblick: Mock, Stub, Spy, Fake, Dummy

Der Sammelbegriff für alle Ersatz-Objekte ist Test Double (im ISTQB-Glossar: "Testdouble"). Martin Fowler hat fünf Typen unterschieden, die heute Industriestandard sind:

TypZweckWas es verifiziertBeispiel-Einsatz
DummyParameterliste füllen, nie verwendetnichtsEin leeres User-Objekt, das eine Funktion entgegennimmt aber nicht liest
StubVorgefertigte Antwort liefernnichts (State Verification)api.getUser() gibt immer denselben Test-User zurück
SpyAufrufe aufzeichnen, sonst echte FunktionAufrufe nach dem Testjest.spyOn(console, 'log') ruft das echte log, merkt sich aber alle Calls
MockPre-set-Erwartungen mit VerifyAufrufe und State (Behaviour Verification)expect(mockFn).toHaveBeenCalledWith('foo')
FakeVereinfachte echte ImplementierungStateIn-Memory-Datenbank statt Postgres, Sinon-Fake-Timer statt echte Zeit

Die wichtigste Faustregel kommt direkt von Fowler: ein Stub beantwortet Fragen, ein Mock verifiziert Befehle. Bei einem Stub interessiert dich, was nach dem Test in der Welt steht. Bei einem Mock interessiert dich, ob dein Code die richtigen Befehle in der richtigen Reihenfolge an seine Abhängigkeiten geschickt hat.

In Jest verschwimmen die Grenzen. jest.fn() ist gleichzeitig Stub und Mock: du gibst eine Antwort vor und kannst nachher die Aufrufe verifizieren. Das ist pragmatisch, macht aber die akademische Trennung manchmal hinderlich. Wichtig ist nicht der Name, sondern was du im Test verifizierst.

Mocks mit Jest in der Praxis (TypeScript)

Drei Patterns reichen für 90 Prozent der täglichen Test-Arbeit aus: der Modul-Mock, der Funktions-Mock und der Spy. Jeder löst ein anderes Problem.

Pattern 1: Modul-Mock mit jest.mock()

Der häufigste Fall: dein Code importiert ein Modul, das mit der Außenwelt spricht (Datenbank, HTTP-Client, Filesystem). Du willst den kompletten Modul-Inhalt durch eine Test-Variante ersetzen. jest.mock() macht genau das.

Listing 1: Modul-Mock einer API-Client-Datei in Jest
// src/services/api.ts
export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// src/services/user-service.test.ts
import { fetchUser } from './api';
import { getUserDisplayName } from './user-service';

jest.mock('./api');

describe('getUserDisplayName', () => {
  it('formatiert Name aus API-Response', async () => {
    (fetchUser as jest.Mock).mockResolvedValue({
      id: '42',
      firstName: 'Wilson',
      lastName: 'Campero',
    });

    const result = await getUserDisplayName('42');

    expect(result).toBe('Wilson Campero');
    expect(fetchUser).toHaveBeenCalledWith('42');
  });
});

Drei Zeilen, drei Effekte: jest.mock('./api') ersetzt das ganze Modul, mockResolvedValue definiert die Antwort, toHaveBeenCalledWith verifiziert den Aufruf. Du brauchst weder Datenbank noch Netzwerk, der Test läuft in wenigen Millisekunden.

Pattern 2: Funktions-Mock mit jest.fn()

Wenn du nur eine einzelne Funktion austauschen willst, oft als Callback oder Dependency-Injection-Parameter, ist jest.fn() die saubere Lösung. Sie erzeugt eine Mock-Funktion, die nichts tut, aber sich alle Aufrufe merkt.

Listing 2: Funktions-Mock als Dependency-Injection in einem Event-Handler
// src/handlers/order-handler.ts
type Logger = (message: string) => void;

export function processOrder(orderId: string, log: Logger): void {
  log(`Order ${orderId} verarbeitet`);
}

// src/handlers/order-handler.test.ts
import { processOrder } from './order-handler';

describe('processOrder', () => {
  it('loggt die Order-ID', () => {
    const logSpy = jest.fn();

    processOrder('ORD-7421', logSpy);

    expect(logSpy).toHaveBeenCalledTimes(1);
    expect(logSpy).toHaveBeenCalledWith('Order ORD-7421 verarbeitet');
  });
});

Der Trick liegt in der Architektur: processOrder bekommt den Logger als Parameter, nicht als globalen Import. Diese Dependency-Injection macht den Code testbar ohne jest.mock(). Wenn dein Code schwer zu mocken ist, ist das oft ein Architektur-Signal, kein Test-Problem.

Pattern 3: Spy mit jest.spyOn()

Manchmal willst du eine echte Funktion nicht ersetzen, sondern nur beobachten. Beispiel: du willst sichergehen, dass console.error bei einem Fehler aufgerufen wurde, willst aber das tatsächliche Logging behalten. jest.spyOn() ist genau dafür gebaut.

Listing 3: Spy auf console.error ohne die echte Logging-Logik zu ersetzen
// src/services/payment.ts
export function chargeCard(amount: number): boolean {
  if (amount <= 0) {
    console.error(`Ungültiger Betrag: ${amount}`);
    return false;
  }
  return true;
}

// src/services/payment.test.ts
import { chargeCard } from './payment';

describe('chargeCard', () => {
  it('loggt Fehler bei negativem Betrag', () => {
    const errorSpy = jest.spyOn(console, 'error').mockImplementation();

    const result = chargeCard(-10);

    expect(result).toBe(false);
    expect(errorSpy).toHaveBeenCalledWith('Ungültiger Betrag: -10');

    errorSpy.mockRestore();
  });
});

Das .mockImplementation() verhindert, dass die echte Error-Nachricht den Test-Output verschmutzt. mockRestore() am Ende stellt das Original wieder her, damit der nächste Test sauber startet. Wenn du das vergisst, leakt der Spy in andere Tests, was zu sehr verwirrenden Fehlern führt.

Wann Mock, wann Stub, wann Fake? Der Entscheidungsbaum

In der Praxis stellst du dir vor jedem Test eine Frage: was will ich eigentlich beweisen? Die Antwort entscheidet, welches Test-Double passt.

SituationTest-DoubleJest-Werkzeug
Ich will nur prüfen, ob eine Funktion aufgerufen wurdeSpyjest.spyOn()
Ich brauche eine vorgefertigte Antwort, kein VerifyStubjest.fn().mockReturnValue(x)
Ich will Antwort UND Aufrufe verifizierenMockjest.fn() mit toHaveBeenCalledWith
Ich brauche funktionale Logik (In-Memory-DB, Fake-Clock)FakeEigene Klasse oder jest.useFakeTimers()
Ich fülle nur einen Parameter ohne ihn zu nutzenDummy{} as User reicht

Eine zweite Faustregel hilft bei der Wahl: je mehr du verifizierst, desto strenger ist dein Test. Ein Spy ist nachsichtig (Test bricht nur bei Aufruf-Erwartungen), ein Mock ist strikt (Test bricht bei jeder Abweichung). Strikt klingt erstmal gut, ist aber teuer: jeder Refactoring-Schritt kann den Mock invalidieren.

In DBI-Sprints habe ich gelernt: starte nachsichtig, werde strenger bei den Geschäfts-Kernpfaden. Login, Bezahlung, Datenmigration brauchen strikte Mocks. UI-Helfer und Format-Funktionen reichen mit Stubs oder ganz ohne Mocking.

Anti-Patterns beim Mocking

Mocks lösen Probleme. Falsch eingesetzt erzeugen sie neue. Diese vier Anti-Patterns sehe ich in fast jedem Code-Review.

Over-Mocking. Du mockst alles, was nicht deine eigene Datei ist. Das Ergebnis: dein Test prüft die Mocks, nicht den Code. Wenn der echte Code intern refactored wird, läuft der Test grün, obwohl die Funktion kaputt ist. Faustregel: mocke an Systemgrenzen (Datenbank, externe API, Filesystem), nicht an internen Modul-Grenzen.

Mock-Implementation-Drift. Dein Mock liefert { status: 'OK' }, die echte API liefert seit drei Wochen { state: 'ok' }. Tests sind grün, Production crasht. Lösung: Contract-Tests (z.B. mit Pact) oder Integration-Tests auf der Mock-Grenze, die regelmäßig gegen die echte API laufen.

Brittle Tests durch Über-Verifikation. Du schreibst expect(mockFn).toHaveBeenCalledTimes(3). Drei Jahre später refactored jemand die Implementierung von drei auf zwei Aufrufe, ohne Verhalten zu ändern. Der Test bricht, der Code ist korrekt. Verifiziere nur, was für das Verhalten relevant ist, nicht jede Implementierungs-Detail.

Mocking deines eigenen Codes. Wenn du Funktionen aus derselben Datei oder demselben Service mocken musst, hat dein Modul oft zu viele Verantwortlichkeiten. Statt zu mocken: aufteilen. Test-Schmerz ist ein Architektur-Signal.

Du brauchst Unterstützung beim Aufbau einer schnellen Test-Suite? Unsere Berater bringen Erfahrung aus Sprints, in denen Test-Laufzeiten von 14 Minuten auf unter 30 Sekunden gedrückt wurden. Beratung Testautomatisierung anfragen.

Fazit: Mocking ist Vertrags-Gestaltung, nicht Test-Trick

Mocks, Stubs, Spies und Fakes sind keine Testing-Akrobatik, sondern Verträge zwischen deinem Code und seinen Abhängigkeiten. Du beschreibst im Test, was dein Code von außen erwartet, und überprüfst, ob er sich an seinen Teil hält.

Tests sind kein Add-on. Tests sind der Vertrag. Mit Jest und sauberen Dependency-Injection-Patterns schreibst du diese Verträge in Sekunden, nicht in Stunden.

Der nächste Schritt: schau dir deine längsten Tests an, identifiziere die echten Abhängigkeiten und ersetze sie durch das passende Test-Double. Eine 14-Minuten-Suite wird so realistisch zur 30-Sekunden-Suite.

Häufige Fragen zu Mocking (FAQ)

Was ist der Unterschied zwischen Mock und Stub?

Ein Stub liefert vorgefertigte Antworten und prüft nichts. Ein Mock liefert Antworten und verifiziert zusätzlich, wie er aufgerufen wurde. Martin Fowler fasst das in der Formel zusammen: Stubs beantworten Fragen, Mocks verifizieren Befehle. In Jest sind die Begriffe verschwommen, weil jest.fn() beides kann.

Brauche ich Jest, um in JavaScript zu mocken?

Nein. Jest ist die populärste Wahl im JavaScript-Ökosystem, aber Vitest, Mocha plus Sinon oder Node.js native Test-Runner ab Version 18 können das alle. Die Konzepte sind identisch, nur die Syntax unterscheidet sich.

Wann sollte ich nicht mocken?

Für reine Format-Funktionen, mathematische Berechnungen oder Validierungen ohne externe Abhängigkeiten brauchst du kein Mocking. Für Integration-Tests, die ganz bewusst echte Systeme verbinden sollen, ist Mocking ebenfalls falsch. Mock-Use ist auf Unit-Tests an Systemgrenzen begrenzt.

Was ist der Unterschied zwischen jest.fn() und jest.spyOn()?

jest.fn() erzeugt eine komplett neue Mock-Funktion ohne Original-Verhalten. jest.spyOn() nimmt eine existierende Methode eines Objekts, beobachtet sie und ruft das Original weiter auf, sofern du es nicht mit mockImplementation() überschreibst. Nutze spyOn, wenn du das echte Verhalten behalten willst.

Kann ich auch Klassen mocken?

Ja, mit jest.mock() auf dem Modul, das die Klasse exportiert. Jest erzeugt automatisch eine Mock-Version der Klasse, in der alle Methoden jest.fn() sind. Für komplexere Fälle nutze mockImplementation(() => new FakeClass()) mit einer eigenen Test-Klasse.

Wie verifiziere ich, dass eine Async-Funktion aufgerufen wurde?

Genauso wie bei sync-Funktionen, mit toHaveBeenCalledWith. Wichtig ist nur, dass dein Test selbst async ist und du den getesteten Aufruf mit await auflöst, bevor du die Erwartung schreibst. Sonst verifizierst du, bevor der Promise resolved hat.

Geht Mockito-Syntax auch in JavaScript?

Nein, Mockito ist Java-spezifisch und an die Mockito-Annotations gebunden. Im JavaScript-Ökosystem ist Jest der De-facto-Standard. Wenn du aus der Java-Welt kommst und Mockito-ähnliche Syntax suchst, schau dir ts-mockito für TypeScript an, das ein ähnliches Fluent-API anbietet.

Testautomatisierung Beratung

Sie möchten Ihre Testautomatisierung optimieren? Unsere Experten helfen Ihnen bei der Auswahl der richtigen Tools, Best Practices und CI/CD-Integration.

Jetzt anfragen

Finden Sie weitere interessante Artikel zum Thema: