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.
Inhaltsverzeichnis
- 1. Warum native Module in Jest überhaupt gemockt werden müssen
- 2. Grundlagen: jest.mock und die __mocks__-Konvention
- 3. Praktisches Beispiel: ein Kamera-Modul mocken
- 4. TurboModules und NativeEventEmitter mocken
- 5. Mocks zentral in jest.config und setup-Dateien registrieren
- 6. Fehlerfälle systematisch abdecken
- 7. Grenzen reiner Unit-Tests bei nativer Funktionalität
- 8. Wann ein E2E-Test statt eines Mocks notwendig ist
- 9. Mocks pflegen: Drift zwischen Mock und echtem Modul vermeiden
- 10. Zusammenfassung
- 11. FAQ
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.