Pengujian komponen React Native yang melibatkan pemanggilan network sering kali menjadi sumber utama flaky test di Continuous Integration (CI). Masalah ini biasanya berakar pada pendekatan mocking manual terhadap modul seperti global.fetch atau instance axios. Mengganti implementasi network manual dengan Mock Service Worker (MSW) v2 menyediakan lapisan intersepsi berbasis standar Fetch API yang deterministik dan terisolasi.
Problem State Leakage pada Mocking Manual
Penggunaan jest.spyOn(global, 'fetch') atau jest.mock('axios') memanipulasi objek global secara langsung di level JavaScript runtime. Pendekatan ini rentan menimbulkan beberapa masalah kritis:
- State Leakage antar-test: Mutasi mock yang diubah pada satu blok pengujian sering kali tidak ter-reset secara sempurna saat berpindah ke pengujian berikutnya, terutama jika terjadi unhandled rejection atau eksekusi asinkron yang tertunda.
- Coupling terhadap implementasi teknis: Jika aplikasi beralih dari
axioskefetch, seluruh mock suite rusak meskipun kontrak API dan antarmuka komponen tidak berubah. - Respon asinkron yang tidak realistis: Mock manual sering kali mengabaikan lifecycle HTTP standar seperti header resolution, parsing stream body, dan status code handling secara natural.
MSW v2 mengatasi masalah ini dengan mencegat request pada network layer Node.js (via @mswjs/interceptors), membiarkan kode aplikasi mengeksekusi path HTTP klien aslinya tanpa modifikasi global object internal aplikasi.
Konfigurasi Polyfill Web API di Jest
MSW v2 mengadopsi standar Web Fetch API (Request, Response, Headers, TextEncoder). Lingkungan pengujian React Native di Jest secara default belum mengekspos API ini secara lengkap. Diperlukan injeksi polyfill sebelum MSW diinisialisasi.
Pasang dependensi yang dibutuhkan:
npm install --save-dev msw undiciTambahkan konfigurasi polyfill pada file setup Jest, misalnya jest.polyfills.js:
// jest.polyfills.js
import { TextDecoder, TextEncoder } from 'node:util';
import { ReadableStream } from 'node:stream/web';
import { fetch, Headers, FormData, Request, Response } from 'undici';
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
ReadableStream: { value: ReadableStream },
fetch: { value: fetch, writable: true, configurable: true },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
});Daftarkan file tersebut di jest.config.js pada bagian setupFiles agar dieksekusi sebelum environment Jest dimuat:
// jest.config.js
module.exports = {
preset: 'react-native',
setupFiles: ['./jest.polyfills.js'],
setupFilesAfterEnv: ['./jest.setup.ts'],
};Inisialisasi Server MSW v2 dan Lifecycle Jest
Definisikan handler dan inisialisasi server menggunakan API MSW v2 (http dan HttpResponse).
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('https://api.example.com/user', () => {
return HttpResponse.json({
id: 'usr-123',
name: 'Budi Santoso',
email: '[email protected]',
});
}),
];// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);Atur lifecycle server di dalam jest.setup.ts. Lifecycle ini memastikan bahwa setiap test suite berjalan di lingkungan network yang bersih dan setiap unhandled request akan langsung menggagalkan test runner.
// jest.setup.ts
import { server } from './src/mocks/server';
beforeAll(() => {
// onUnhandledRequest: 'error' mencegah silent network pass-through di CI
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
// Menghapus runtime override handler agar tidak bocor ke test lain
server.resetHandlers();
});
afterAll(() => {
server.close();
});Pengujian Komponen Menggunakan React Native Testing Library
Berikut implementasi komponen sederhana yang mengonsumsi endpoint data pengguna:
// src/components/UserProfile.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
interface User {
name: string;
email: string;
}
export const UserProfile = () => {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/user')
.then((res) => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then((data) => setUser(data))
.catch(() => setError(true))
.finally(() => setLoading(false));
}, []);
if (loading) return <ActivityIndicator testID="loading-indicator" />;
if (error) return <Text testID="error-message">Gagal memuat profil.</Text>;
if (!user) return null;
return (
<View>
<Text testID="user-name">{user.name}</Text>
<Text testID="user-email">{user.email}</Text>
</View>
);
};Tulis unit test untuk memvalidasi skenario sukses dan skenario server error (500) menggunakan server.use() untuk runtime override.
// src/components/__tests__/UserProfile.test.tsx
import React from 'react';
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
import { http, HttpResponse } from 'msw';
import { server } from '../../mocks/server';
import { UserProfile } from '../UserProfile';
describe('UserProfile Component', () => {
it('menampilkan data pengguna saat fetch berhasil', async () => {
render(<UserProfile />);
// Tunggu loading spinner selesai di-unmount
await waitForElementToBeRemoved(() => screen.getByTestID('loading-indicator'));
expect(screen.getByTestID('user-name')).toHaveTextContent('Budi Santoso');
expect(screen.getByTestID('user-email')).toHaveTextContent('[email protected]');
});
it('menampilkan pesan error jika server mengembalikan status 500', async () => {
// Override handler khusus untuk skenario error di suite ini
server.use(
http.get('https://api.example.com/user', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserProfile />);
await waitForElementToBeRemoved(() => screen.getByTestID('loading-indicator'));
expect(screen.getByTestID('error-message')).toHaveTextContent('Gagal memuat profil.');
});
});Best Practices dan Pencegahan Flaky Test di CI
- Wajib Aktifkan
onUnhandledRequest: 'error': Opsi ini menghentikan eksekusi test runner seketika apabila komponen memicu HTTP request yang belum didefinisikan di handler. Ini mencegah request bocor ke server aktual atau timeout tanpa deskripsi jelas. - Gunakan
server.resetHandlers()diafterEach: Jangan menghapus atau me-reset handler di dalam blokitsecara manual. Penanganan di level hookafterEachmenjamin handler runtime dariserver.use()selalu dibersihkan meski assertion test sebelumnya gagal. - Hindari Konflik Fake Timers: Penggunaan
jest.useFakeTimers()dapat menahan scheduler internal MSW dan fetch polyfill. Jika fake timers dibutuhkan untuk animasi atau debounce, jalankan pemanggilan network terlebih dahulu sebelum memajukan clock viajest.advanceTimersByTime().
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!