Akar Masalah: Mengapa Error 'NativeModule is null' Terjadi di Jest
Saat menjalankan unit test menggunakan Jest di proyek React Native, eksekusi kode berjalan di dalam lingkungan Node.js (V8 runtime), bukan pada virtual machine Android (JVM/ART) atau runtime iOS (Objective-C/Swift runtime). Akibatnya, modul native yang bergantung pada jembatan platform (Bridge) maupun antarmuka C++ JSI (JavaScript Spec Interface) pada New Architecture tidak tersedia.
Pada arsitektur lama, React Native mengandalkan objek global NativeModules. Jika modul tidak terpasang di runtime pengujian, pemanggilan terhadap properti modul tersebut menghasilkan undefined, yang memicu runtime crash:
TypeError: Cannot read property 'myMethod' of undefined
TypeError: NativeModule is nullPada New Architecture, TurboModule menggunakan TurboModuleRegistry.getEnforcing(...). Alih-alih mengembalikan null secara diam-diam, fungsi ini secara eksplisit melempar Invariant Violation jika modul native yang diminta belum diregistrasikan. Untuk mengeksekusi test suite secara konsisten dan cepat tanpa menyalakan emulator, Anda harus mengisolasi ketergantungan native melalui teknik stubbing dan centralized mocking.
Definisi TurboModule Spec dengan TypeScript
TurboModule mengandalkan kontrak tipe statis (Codegen Spec) untuk menjembatani kode JavaScript dan native C++. Berikut adalah contoh kontrak modul kalkulasi dan analitik (NativeAnalyticsSpec.ts):
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
trackEvent(eventName: string, payload: Object): Promise<boolean>;
getSessionId(): string;
multiply(a: number, b: number): Promise<number>;
addListener(eventName: string): void;
removeListeners(count: number): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeAnalytics');Implementasi Centralized Mock via jest.setup.ts
Membuat mock ad-hoc di setiap file pengujian melanggar prinsip DRY dan meningkatkan waktu maintenance test suite. Gunakan file setup Jest (misalnya jest.setup.ts) yang didaftarkan melalui konfigurasi setupFilesAfterEnv di jest.config.js untuk mencegat pemanggilan TurboModuleRegistry secara terpusat.
// jest.config.js
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
};Di dalam jest.setup.ts, stub metode getEnforcing dan get milik TurboModuleRegistry untuk mengembalikan mock implementation yang sesuai dengan kontrak tipe Spec:
// jest.setup.ts
import { TurboModuleRegistry } from 'react-native';
import type { Spec as NativeAnalyticsSpec } from './src/specs/NativeAnalyticsSpec';
export const mockNativeAnalytics: jest.Mocked<NativeAnalyticsSpec> = {
trackEvent: jest.fn().mockResolvedValue(true),
getSessionId: jest.fn().mockReturnValue('mock-session-xyz'),
multiply: jest.fn().mockImplementation((a: number, b: number) => Promise.resolve(a * b)),
addListener: jest.fn(),
removeListeners: jest.fn(),
};
// Simpan implementasi asli jika diperlukan fallback
const originalGetEnforcing = TurboModuleRegistry.getEnforcing;
const originalGet = TurboModuleRegistry.get;
jest.spyOn(TurboModuleRegistry, 'getEnforcing').mockImplementation((name: string) => {
if (name === 'NativeAnalytics') {
return mockNativeAnalytics;
}
return originalGetEnforcing(name);
});
jest.spyOn(TurboModuleRegistry, 'get').mockImplementation((name: string) => {
if (name === 'NativeAnalytics') {
return mockNativeAnalytics;
}
return originalGet(name);
});Pengujian Komponen, Async Methods, dan Event Emitter
Pengujian modul native mencakup dua skenario utama: pemanggilan fungsi asinkron berbasis Promise dan pemantauan event native via NativeEventEmitter.
1. Verifikasi Async Bridge Methods
Berikut implementasi service yang mengonsumsi TurboModule:
// src/analyticsService.ts
import NativeAnalytics from './specs/NativeAnalyticsSpec';
export async function recordPurchase(itemId: string, price: number): Promise<boolean> {
if (!itemId || price <= 0) {
throw new Error('Invalid purchase payload');
}
return await NativeAnalytics.trackEvent('PURCHASE_COMPLETED', { itemId, price });
}Uji interaksi service tersebut dengan memverifikasi parameter pemanggilan dan hasil resolusi Promise:
// __tests__/analyticsService.test.ts
import { recordPurchase } from '../src/analyticsService';
import { mockNativeAnalytics } from '../jest.setup';
describe('recordPurchase', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('harus memanggil native trackEvent dengan payload yang valid', async () => {
mockNativeAnalytics.trackEvent.mockResolvedValueOnce(true);
const result = await recordPurchase('item-101', 50000);
expect(mockNativeAnalytics.trackEvent).toHaveBeenCalledTimes(1);
expect(mockNativeAnalytics.trackEvent).toHaveBeenCalledWith('PURCHASE_COMPLETED', {
itemId: 'item-101',
price: 50000,
});
expect(result).toBe(true);
});
it('harus melempar error jika argumen tidak valid tanpa menyentuh native layer', async () => {
await expect(recordPurchase('', 0)).rejects.toThrow('Invalid purchase payload');
expect(mockNativeAnalytics.trackEvent).not.toHaveBeenCalled();
});
});2. Mocking NativeEventEmitter
Ketika modul native mengirimkan event ke layer JavaScript melalui NativeEventEmitter, Jest memerlukan mock terhadap class tersebut guna mencegah error new NativeEventEmitter() requires a non-null argument.
// jest.setup.ts (Tambahan untuk Event Emitter)
const mockSubscription = { remove: jest.fn() };
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
return jest.fn().mockImplementation(() => ({
addListener: jest.fn().mockReturnValue(mockSubscription),
removeAllListeners: jest.fn(),
removeSubscription: jest.fn(),
emit: jest.fn(),
}));
});Strategi Menjaga Kecepatan Test Suite
- Hindari Inisialisasi Runtime Eksternal: Jalankan mock sepenuhnya in-memory menggunakan stub
jest.fn(). Menghubungkan test suite unit ke dependensi C++ atau Java native bindings akan merusak isolasi dan memperlambat waktu eksekusi Jest secara drastis. - Reset State Antar Test: Terapkan
jest.clearAllMocks()pada blokbeforeEachuntuk membersihkan metadata pemanggilan mock tanpa menghapus implementasi yang telah dikonfigurasi dijest.setup.ts. - Gunakan TypeScript Strict Mocking: Hindari casting type ke
anysaat membuat stub module. Gunakan utilitas bawaanjest.Mocked<Spec>untuk memastikan setiap signature method tetap tersinkronisasi dengan Codegen Spec aplikasi saat terjadi perubahan antarmuka native.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!