Native Module isoliert testen: Mock-Strategien für Jest in React Native
AI generated
RN
native
React Native · Testing · Native Modules
Native Module isoliert testen: Mock-Strategien für Jest
Native Funktionalität testbar machen, ohne echten nativen Code auszuführen

Ein Jest-Testlauf findet auf Node.js statt und kann weder Swift- noch Kotlin-Code ausführen, trotzdem hängt viel React-Native-Logik direkt von nativen Modulen ab: Kamera, Standort, Biometrie, Push-Benachrichtigungen. Wer diese Abhängigkeiten nicht sauber mockt, landet entweder bei unlösbaren Testfehlern oder bei Tests, die stillschweigend gar nichts prüfen. Dieser Artikel zeigt konkrete Mock-Strategien für native Module in Jest, ein durchgängiges Beispiel mit einem Kamera-Modul, und wo die Grenze zwischen sinnvollem Unit-Test und notwendigem End-to-End-Test tatsächlich verläuft.

14 Min. Lesezeit Jest Native Module Mocking Unit Testing TurboModules

1. Warum native Module in Jest überhaupt gemockt werden müssen

Jest führt JavaScript- beziehungsweise TypeScript-Code in einer Node.js-Umgebung aus, ohne iOS-Simulator, ohne Android-Emulator und ohne Zugriff auf echte native Bibliotheken. Ein NativeModules-Aufruf, der im echten App-Kontext an nativen Swift- oder Kotlin-Code weitergereicht wird, trifft in Jest schlicht ins Leere, weil die Bridge, über die React Native JavaScript- und native Seite verbindet, in der Testumgebung gar nicht existiert. Ohne Mock schlägt ein solcher Aufruf entweder mit einer Fehlermeldung fehl oder liefert undefined zurück, je nachdem, wie robust der jeweilige Modul-Wrapper geschrieben ist.

Mocking löst dieses Problem, indem es die native Schnittstelle durch eine JavaScript-Attrappe ersetzt, die sich an der öffentlichen API des echten Moduls orientiert, aber vollständig innerhalb von Jest lauffähig ist. Der Sinn eines solchen Mocks ist nicht, native Funktionalität zu simulieren, sondern die JavaScript-seitige Logik, die auf das native Modul aufbaut, isoliert von der tatsächlichen Hardware und den Betriebssystem-APIs zu testen: Wird bei einem Kamerafehler die richtige Fehlermeldung angezeigt, wird ein Standort korrekt im UI formatiert, wird ein Biometrie-Fallback ausgelöst, wenn der Sensor nicht verfügbar ist.

2. Grundlagen: jest.mock und die __mocks__-Konvention

Jest bietet zwei gängige Wege, ein Modul zu mocken: einen Inline-Aufruf von jest.mock() direkt in der Testdatei, oder eine dedizierte Mock-Datei in einem __mocks__-Verzeichnis neben dem echten Modul, die Jest automatisch einbindet. Für native React-Native-Module hat sich die zweite Variante bewährt, weil ein natives Modul üblicherweise in vielen verschiedenen Testdateien verwendet wird und ein zentraler Mock verhindert, dass jede Testdatei ihre eigene, potenziell inkonsistente Attrappe pflegt.

Wichtig ist, den Mock so nah wie möglich an der tatsächlichen API-Signatur des nativen Moduls zu halten, inklusive Promise-basierter Rückgabewerte, falls die echte Bridge asynchron arbeitet. Ein Mock, der synchron einen Wert zurückgibt, während das echte Modul ein Promise liefert, führt in Tests zu einem trügerischen Erfolg, der in der echten App als unbehandeltes await-Problem wieder auftaucht, weil der Testcode nie gelernt hat, korrekt auf die asynchrone Antwort zu warten.

3. Praktisches Beispiel: ein Kamera-Modul mocken

Als durchgängiges Beispiel dient ein Kamera-Modul, das ein Foto aufnimmt und den Dateipfad zurückliefert, oder bei fehlender Berechtigung einen spezifischen Fehler wirft. Die Komponente, die dieses Modul nutzt, muss unter anderem korrekt auf einen erfolgreichen Aufnahmevorgang, auf einen Berechtigungsfehler und auf einen generischen Hardwarefehler reagieren. Ein zentraler Mock in __mocks__/react-native-camera-module.ts bildet alle drei Fälle als konfigurierbare Funktionen ab, die im jeweiligen Test gezielt auf das gewünschte Verhalten umgeschaltet werden.

Entscheidend ist, dass der Mock als jest.fn() implementiert wird, nicht als starre Funktion, damit einzelne Tests das Rückgabeverhalten pro Testfall über mockResolvedValueOnce oder mockRejectedValueOnce anpassen können. So lässt sich mit demselben Mock sowohl der Erfolgsfall als auch jeder relevante Fehlerfall abdecken, ohne für jeden Fall eine eigene Mock-Datei zu pflegen.


// __mocks__/react-native-camera-module.ts
export const CameraError = {
  PERMISSION_DENIED: "PERMISSION_DENIED",
  HARDWARE_UNAVAILABLE: "HARDWARE_UNAVAILABLE",
} as const;

export const takePhoto = jest.fn(async (): Promise<{ uri: string }> => {
  return { uri: "file:///mock/photo.jpg" };
});

export const requestCameraPermission = jest.fn(async (): Promise<boolean> => {
  return true;
});

// Beispieltest: Berechtigungsfehler korrekt behandeln
import { takePhoto, requestCameraPermission, CameraError } from "react-native-camera-module";
import { render, fireEvent, screen, waitFor } from "@testing-library/react-native";
import { CaptureScreen } from "../CaptureScreen";

it("shows a permission error message when the camera is denied", async () => {
  (requestCameraPermission as jest.Mock).mockResolvedValueOnce(false);
  (takePhoto as jest.Mock).mockRejectedValueOnce(
    new Error(CameraError.PERMISSION_DENIED)
  );

  render(<CaptureScreen />);
  fireEvent.press(screen.getByTestId("capture-button"));

  await waitFor(() => {
    expect(screen.getByText(/kamerazugriff verweigert/i)).toBeTruthy();
  });
});

4. TurboModules und NativeEventEmitter mocken

Für Module, die über die neue TurboModule-Architektur eingebunden sind, unterscheidet sich das Mocking im Grundsatz kaum: Der TurboModule-Spec-Typ definiert eine klare TypeScript-Schnittstelle, und der Jest-Mock muss lediglich dieselbe Schnittstelle synchron oder asynchron nachbilden. Der Unterschied liegt hauptsächlich in der Import-Struktur, da TurboModules meist über einen generierten Codegen-Layer eingebunden werden, den ein Mock über jest.mock() an derselben Importstelle abfangen muss, an der die Komponente das Modul lädt.

Module, die auf einem NativeEventEmitter basieren, etwa für kontinuierliche Standort-Updates, brauchen zusätzlich eine gemockte Emitter-Implementierung, die addListener und removeListener korrekt simuliert. Ein einfacher Ansatz ist ein Test-Double, das registrierte Callbacks in einem Array sammelt und über eine Test-Hilfsfunktion wie emitMockLocationUpdate(coords) gezielt auslöst, sodass ein Test exakt kontrollieren kann, wann und mit welchen Daten ein Standort-Event eintrifft.


// __mocks__/react-native-geolocation-module.ts
type LocationCallback = (coords: { lat: number; lng: number }) => void;

const listeners: LocationCallback[] = [];

export const addLocationListener = jest.fn((callback: LocationCallback) => {
  listeners.push(callback);
  return { remove: () => {
    const idx = listeners.indexOf(callback);
    if (idx !== -1) listeners.splice(idx, 1);
  }};
});

// Test-Hilfsfunktion, nur im Mock verfügbar
export const __emitMockLocationUpdate = (coords: { lat: number; lng: number }) => {
  listeners.forEach((cb) => cb(coords));
};

// Im Test:
import { __emitMockLocationUpdate } from "react-native-geolocation-module";

it("updates the displayed coordinates on a location event", async () => {
  render(<LocationScreen />);
  __emitMockLocationUpdate({ lat: 52.52, lng: 13.405 });
  expect(await screen.findByText("52.52, 13.405")).toBeTruthy();
});

5. Mocks zentral in jest.config und setup-Dateien registrieren

Statt jest.mock() in jeder einzelnen Testdatei zu wiederholen, lassen sich häufig genutzte native Module zentral in einer setupFilesAfterEach- beziehungsweise setupFiles-Datei registrieren, die Jest bei jedem Testlauf automatisch einbindet. Das reduziert Duplikation erheblich und sorgt dafür, dass ein neu hinzugefügtes natives Modul nicht in zwanzig verschiedenen Testdateien nachgezogen werden muss, sondern an einer zentralen Stelle gepflegt wird.

Für Module aus dem React-Native-Core-Paket selbst, etwa AsyncStorage oder NetInfo, existieren oft bereits offizielle Jest-Preset-Mocks, die über jest-config-preset react-native eingebunden werden und die meisten Standardfälle bereits sinnvoll abdecken. Eigene native Module und Module aus weniger verbreiteten Drittanbieter-Bibliotheken benötigen dagegen fast immer einen selbst geschriebenen Mock, weil kein offizielles Preset existiert.

6. Fehlerfälle systematisch abdecken

Ein häufiger Fehler beim Mocken nativer Module ist, ausschließlich den Erfolgsfall zu testen und Fehlerpfade zu ignorieren, obwohl gerade native Module in der Praxis viele Fehlerquellen mitbringen: fehlende Berechtigung, nicht verfügbare Hardware, Timeout bei einer Standortanfrage, abgebrochene Aufnahme durch den Nutzer. Jeder dieser Fälle sollte als eigener Testfall mit einem entsprechend konfigurierten Mock abgebildet werden, damit die UI-Logik für jeden Fehlerfall nachweislich korrekt reagiert.

Ein systematischer Ansatz ist, für jedes native Modul eine kleine Tabelle der möglichen Fehlercodes zu führen und für jeden Code mindestens einen Test zu schreiben, der prüft, ob die Komponente eine sinnvolle Nutzerrückmeldung anzeigt. Diese Disziplin zahlt sich besonders bei Kamera- und Standortfunktionen aus, weil Nutzer Berechtigungen jederzeit über die Systemeinstellungen entziehen können und die App darauf robust reagieren muss, nicht nur beim allerersten Zugriff.

7. Grenzen reiner Unit-Tests bei nativer Funktionalität

So sorgfältig ein Mock auch gestaltet ist, er prüft niemals, ob das echte native Modul tatsächlich funktioniert, sondern ausschließlich, ob die JavaScript-Logik korrekt auf definierte Rückgabewerte reagiert. Ob eine Kameraberechtigung auf einem echten Android-14-Gerät tatsächlich korrekt angefragt wird, ob das native Modul auf einem bestimmten Gerätemodell abstürzt, oder ob eine Bridge-Serialisierung bei sehr großen Bilddaten fehlschlägt, bleibt für Unit-Tests grundsätzlich unsichtbar.

Diese Lücke lässt sich nur durch End-to-End-Tests mit echten Geräten oder Simulatoren schließen, etwa mit Detox oder Maestro, die tatsächlich mit dem realen nativen Code interagieren. Ein sinnvoller Testaufbau kombiniert deshalb beide Ebenen bewusst: viele schnelle, isolierte Jest-Tests für die JavaScript-Logik und deren Fehlerbehandlung, ergänzt um wenige, aber gezielte E2E-Tests für die kritischsten nativen Interaktionen wie den tatsächlichen Kamera-Berechtigungsdialog auf einem echten Gerät.

8. Wann ein E2E-Test statt eines Mocks notwendig ist

Ein guter Faustregel-Test ist die Frage, ob ein Fehler in der Interaktion zwischen JavaScript und nativem Code selbst liegen könnte, statt ausschließlich in der JavaScript-Logik. Eine falsch formatierte Fehlermeldung im UI ist ein klassischer Fall für einen Unit-Test mit Mock, ein Absturz beim Serialisieren eines großen Kamerabilds über die Bridge dagegen nur durch einen echten Lauf auf einem Gerät oder Simulator erkennbar, weil dabei tatsächlich native Speicher- und Serialisierungsmechanismen greifen.

Besonders kritisch sind Fälle, in denen sich native APIs zwischen Betriebssystemversionen ändern, etwa neue Berechtigungsmodelle in aktuellen Android- oder iOS-Versionen. Ein Mock bildet immer den zum Zeitpunkt seiner Erstellung bekannten Vertrag ab und merkt nicht automatisch, wenn sich die reale native API geändert hat. Ein periodischer E2E-Testlauf auf aktuellen Betriebssystemversionen ist deshalb die einzige verlässliche Absicherung gegen solche stillen Vertragsbrüche zwischen Mock und Realität.

9. Mocks pflegen: Drift zwischen Mock und echtem Modul vermeiden

Ein Mock, der nie an Änderungen des echten Moduls angepasst wird, driftet mit der Zeit unbemerkt von der tatsächlichen API weg, etwa wenn ein neues Pflichtfeld im Rückgabewert ergänzt wird, der Mock aber weiterhin die alte, unvollständige Struktur zurückliefert. Tests bleiben in diesem Fall grün, obwohl der reale Code in der App längst fehlschlagen würde, was ein trügerisches Sicherheitsgefühl erzeugt, das schwerer wiegt als gar keine Tests zu haben.

Ein bewährter Gegenmaßnahme ist, den TypeScript-Typ des Mocks explizit gegen den echten Modul-Typ zu prüfen, etwa über ein satisfies-Constraint oder einen expliziten Type-Import aus der echten Modul-Definition. Ändert sich die echte Schnittstelle, schlägt die TypeScript-Kompilierung des Mocks fehl, statt dass die Diskrepanz erst zur Laufzeit in der Produktion auffällt. Diese Kopplung zwischen Mock-Typ und echtem Modul-Typ ist der wirksamste Schutz gegen unbemerkte Drift.


import type { CameraModuleSpec } from "react-native-camera-module";

// satisfies stellt sicher, dass der Mock exakt zur echten Modul-Signatur passt
export const takePhoto = jest.fn(async () => ({
  uri: "file:///mock/photo.jpg",
})) satisfies CameraModuleSpec["takePhoto"];
Testebene Prüft Werkzeug Geeignet für
Reiner Unit-Test mit Mock JavaScript-Logik und Fehlerbehandlung Jest, Testing Library UI-Reaktion auf definierte Rückgabewerte
Mock mit satisfies-Typprüfung Konsistenz zwischen Mock und echter API TypeScript, Jest Verhindert stille Drift bei API-Änderungen
E2E-Test auf Simulator/Emulator Reale Bridge-Kommunikation Detox, Maestro Serialisierung, Berechtigungsdialoge
E2E-Test auf echtem Gerät Hardwareverhalten und OS-Versionen Detox mit Real-Device-Farm Kamera, GPS, Biometrie auf echter Hardware
Manuelles Testen Edge Cases, UX-Feinheiten Testgerät, Beta-Tester Seltene Gerätemodelle, neue OS-Versionen

Mironsoft

React-Native-App-Entwicklung und Magento-Anbindung

Eine mobile App zum Magento-Shop, die wirklich rund läuft?

Wir entwickeln React-Native-Apps, die sauber an die Magento REST- oder GraphQL-API angebunden sind, von der ersten Codezeile bis zur Veröffentlichung im App Store und bei Google Play.

App-Konzeption

Architektur und Feature-Umfang einer Magento-angebundenen App gemeinsam planen.

Magento-API-Integration

Produktkatalog, Warenkorb und Checkout sauber an die Shop-API anbinden.

Store-Veröffentlichung

App Store- und Google-Play-Freigabeprozess ohne Stolperfallen begleiten.

10. Zusammenfassung

Native Module mocken: Das Wichtigste auf einen Blick

Warum mocken

Jest läuft in Node.js ohne native Bridge, native Modul-Aufrufe müssen durch JavaScript-Attrappen ersetzt werden.

Zentrale __mocks__-Dateien

Ein gepflegter Mock pro Modul verhindert Inkonsistenzen zwischen vielen Testdateien.

Fehlerfälle testen

Berechtigungsfehler, Hardwareausfälle und Timeouts brauchen eigene Testfälle, nicht nur der Erfolgsfall.

Grenze zu E2E

Mocks prüfen nur JavaScript-Logik, echte Bridge-Interaktion und Hardware brauchen zusätzliche E2E-Tests.

11. FAQ: Native Module mocken: Das Wichtigste auf einen Blick

1Warum schlägt mein Jest-Test fehl, obwohl die App auf dem Gerät funktioniert?
Wahrscheinlich fehlt ein passender Mock für ein natives Modul, wodurch der Aufruf in der Node.js-Testumgebung ins Leere läuft. Ein zentraler __mocks__-Eintrag für das betroffene Modul behebt dieses Problem in der Regel zuverlässig.
2Muss ich für jedes native Modul einen eigenen Mock schreiben?
Für Module aus dem React-Native-Core wie AsyncStorage existieren oft bereits offizielle Preset-Mocks. Für eigene und weniger verbreitete Drittanbieter-Module ist ein selbst geschriebener Mock fast immer notwendig.
3Wie mocke ich ein Modul, das ein Promise zurückgibt?
Über jest.fn() mit mockResolvedValueOnce für den Erfolgsfall und mockRejectedValueOnce für den Fehlerfall lässt sich das asynchrone Verhalten des echten Moduls präzise nachbilden, inklusive korrekter await-Behandlung im Testcode.
4Ersetzt ein guter Mock den Bedarf an E2E-Tests?
Nein, ein Mock prüft ausschließlich die JavaScript-seitige Logik, niemals die tatsächliche native Implementierung. Kritische native Interaktionen wie Kamera-Berechtigungsdialoge oder Bridge-Serialisierung erfordern weiterhin echte End-to-End-Tests.
5Wie erkenne ich, dass mein Mock von der echten API abgedriftet ist?
Am zuverlässigsten über eine satisfies-Typprüfung gegen den echten Modul-Typ in TypeScript, die bei jeder Abweichung sofort einen Kompilierfehler erzeugt. Ohne diese Kopplung bleibt Drift oft lange unbemerkt.
6Kann ich NativeEventEmitter-basierte Module wie GPS-Updates sinnvoll mocken?
Ja, über eine gemockte addListener-Implementierung, die registrierte Callbacks sammelt, kombiniert mit einer Test-Hilfsfunktion, die Events gezielt auslöst. So lässt sich exakt steuern, wann und mit welchen Daten ein Event im Test eintrifft.
7Wo sollte ich native Modul-Mocks im Projekt ablegen?
Üblich ist ein __mocks__-Verzeichnis neben dem gemockten Modul beziehungsweise auf oberster Projektebene, das Jest automatisch erkennt. Häufig genutzte Mocks lassen sich zusätzlich in einer zentralen setupFiles-Datei registrieren.
8Sind TurboModules schwerer zu mocken als das alte Bridge-Modell?
Nicht wesentlich, da beide letztlich dieselbe JavaScript-Schnittstelle bereitstellen. Der Hauptunterschied liegt in der Import-Struktur über den generierten Codegen-Layer, den der Mock an derselben Stelle abfangen muss.
9Wie viele Fehlerfälle sollte ich pro nativem Modul testen?
Mindestens einen Testfall pro dokumentiertem Fehlercode des Moduls, insbesondere für Berechtigungsverweigerung, Hardwareausfall und Timeout. Diese Fälle treten in der Produktion regelmäßig auf und sollten nicht ungetestet bleiben.
10Lohnt sich der Aufwand für Mocking bei einem kleinen Projekt mit nur einem nativen Modul?
Ja, selbst ein einzelnes natives Modul kann ohne Mock dazu führen, dass ganze Testdateien fehlschlagen oder übersprungen werden müssen. Der Aufwand für einen sauberen Mock ist gering im Vergleich zum Nutzen zuverlässiger Tests.