Pengujian unit pada aplikasi React Native yang menggunakan react-native-mmkv sering menghasilkan flaky test atau bahkan langsung crash di lingkungan Jest. Masalah ini berakar pada perbedaan arsitektur runtime: MMKV bergantung pada JavaScript Interface (JSI) yang mengeksekusi C++ bindings secara sinkron di atas engine JavaScript perangkat, sementara Jest berjalan di lingkungan Node.js tanpa konteks native mobile tersebut.

Ketika pengembang mengabaikan isolasi state storage, data yang ditulis pada satu test case akan bertahan dan bocor ke test case lain. Artikel ini menguraikan arsitektur mock in-memory untuk MMKV dan strategi integrasi lifecycle Jest agar pengujian berjalan deterministik.

Akar Masalah: Ketiadaan JSI di Runtime Node.js

Berbeda dengan AsyncStorage yang memanfaatkan bridge asinkron lama, react-native-mmkv menyuntikkan host object langsung ke runtime JS via JSI (C++). Di Jest, binary C++ native (Android NDK atau iOS objective-C++) tidak dimuat ke dalam runtime Node.js.

Pemanggilan instance new MMKV() tanpa mocking memicu error instan:

TypeError: Cannot read property 'install' of undefined
// atau: Native MMKV methods are not available in this environment

Kesalahan umum berikutnya terjadi saat pengembang menerapkan mock berbasis objek JavaScript statis global (singleton). Jika mock tersebut tidak dibersihkan dengan siklus hidup yang terdefinisi, nilai yang di-set pada Suite A tetap tersimpan ketika Suite B dieksekusi. Urutan eksekusi tes Jest yang bersifat acak atau paralel parsial akan menyebabkan tes gagal secara acak (flaky).

Arsitektur Mock In-Memory Berbasis Map

Mock yang baik harus memenuhi dua kriteria: mengimplementasikan interface sinkron MMKV secara presisi dan mempertahankan isolasi data per instance ID menggunakan struktur data Map di memori.

Simpan implementasi mock ini pada direktori __mocks__/react-native-mmkv.js atau muat langsung di jest.setup.js.

Implementasi Mock Provider

// __mocks__/react-native-mmkv.js
const storageRegistry = new Map();

export class MMKV {
  constructor(configuration = {}) {
    this.instanceId = configuration.id || 'default';
    if (!storageRegistry.has(this.instanceId)) {
      storageRegistry.set(this.instanceId, new Map());
    }
    this.store = storageRegistry.get(this.instanceId);
  }

  set(key, value) {
    this.store.set(key, value);
  }

  getString(key) {
    const value = this.store.get(key);
    return typeof value === 'string' ? value : undefined;
  }

  getNumber(key) {
    const value = this.store.get(key);
    return typeof value === 'number' ? value : undefined;
  }

  getBoolean(key) {
    const value = this.store.get(key);
    return typeof value === 'boolean' ? value : undefined;
  }

  contains(key) {
    return this.store.has(key);
  }

  delete(key) {
    this.store.delete(key);
  }

  getAllKeys() {
    return Array.from(this.store.keys());
  }

  clearAll() {
    this.store.clear();
  }
}

// Utilitas khusus testing untuk reset total environment
export const resetAllMMKVInstances = () => {
  storageRegistry.forEach((internalMap) => internalMap.clear());
  storageRegistry.clear();
};

Konfigurasi Lifecycle di jest.setup.js

Untuk memastikan tidak ada dependensi state antar file uji, daftarkan hooks reset secara global di file konfigurasi Jest.

// jest.setup.js
import { resetAllMMKVInstances } from 'react-native-mmkv';

// Reset registry setiap sebelum dan sesudah test selesai dieksekusi
beforeEach(() => {
  resetAllMMKVInstances();
});

afterEach(() => {
  resetAllMMKVInstances();
  jest.clearAllMocks();
});

Peringatan: Menempatkan fungsi pembersihan hanya pada afterAll tidak cukup. Kegagalan assertions di tengah tes dapat menghentikan blok teardown lokal, sehingga beforeEach global tetap merupakan pengaman utama agar setiap test case dimulai dari state bersih.

Verifikasi Isolasi: Kasus Uji Deterministik

Berikut adalah contoh test suite Jest yang membuktikan bahwa mutasi state di satu case tidak mempengaruhi case berikutnya.

// __tests__/storage-isolation.test.js
import { MMKV } from 'react-native-mmkv';

describe('Isolasi State MMKV Storage', () => {
  const sessionStore = new MMKV({ id: 'session-storage' });
  const appConfigStore = new MMKV({ id: 'config-storage' });

  test('harus menyimpan kredensial pada sessionStore', () => {
    expect(sessionStore.contains('access_token')).toBe(false);
    
    sessionStore.set('access_token', 'ey12345.dummy.token');
    sessionStore.set('login_count', 3);

    expect(sessionStore.getString('access_token')).toBe('ey12345.dummy.token');
    expect(sessionStore.getNumber('login_count')).toBe(3);
  });

  test('harus memastikan sessionStore kosong pada test case berikutnya', () => {
    // Verifikasi state leak: jika lifecycle gagal, access_token masih ada
    expect(sessionStore.contains('access_token')).toBe(false);
    expect(sessionStore.getString('access_token')).toBeUndefined();
    expect(sessionStore.getAllKeys()).toEqual([]);
  });

  test('harus mengisolasi data antar instance MMKV yang berbeda ID', () => {
    sessionStore.set('active_theme', 'dark');
    appConfigStore.set('active_theme', 'light');

    expect(sessionStore.getString('active_theme')).toBe('dark');
    expect(appConfigStore.getString('active_theme')).toBe('light');
  });
});

Batasan Mock In-Memory

Mock in-memory ini menyelesaikan masalah flaky test dan runtime failure di Jest, namun memiliki beberapa batasan teknis yang perlu dipertimbangkan:

  • Multi-process Synchronization: MMKV native mendukung sinkronisasi state antar proses (misal App Extension di iOS). Mock berbasis Map Node.js tidak mereplikasi skenario konkurensi IPC (Inter-Process Communication) native.
  • Enkripsi Storage: Parameter encryptionKey pada instance asli MMKV mengaktifkan AES encryption. Pada mock JS di atas, data disimpan sebagai plaintext di dalam RAM Node.js.
  • Type Coercion: Implementasi native MMKV menolak konversi tipe data yang tidak sesuai. Mock harus mempertahankan pengecekan tipe manual secara ketat jika modul Anda mengandalkan validasi runtime native.

Untuk skenario end-to-end (E2E) yang menuntut keaslian storage engine C++, alihkan pengujian menggunakan framework seperti Detox atau Maestro yang berjalan langsung di atas simulator atau perangkat fisik.