Flaky test pada integration test React Native umumnya bersumber dari dua hal: request HTTP yang lolos ke internet secara tidak sengaja dan state mock yang bocor antar-test case saat menggunakan jest.spyOn atau jest.mock. Pendekatan mock manual di level modul sering mengabaikan implementasi serialisasi HTTP, logic header, dan parsing response.
Alternatif paling ringkas: gunakan jest.mock('./apiClient') jika hanya ingin memverifikasi pemanggilan fungsi. Gunakan Mock Service Worker (MSW) jika ingin menguji integrasi komponen dari UI hingga network layer secara hermetis tanpa mock manual pada level modul.
Setup Polyfill Web API di Environment Jest
MSW v2 bergantung pada standar Fetch API Node (Request, Response, Headers, dan ReadableStream). Preset Jest React Native standar tidak menyertakan polyfill ini secara lengkap.
Install dependensi yang dibutuhkan:
npm install --save-dev msw undiciKonfigurasikan polyfill pada file setup Jest (misalnya jest.setup.ts) sebelum inisialisasi MSW server:
// jest.setup.ts
import { TextEncoder, TextDecoder } from 'util';
import { ReadableStream } from 'node:stream/web';
import { fetch, Headers, Request, Response, FormData } from 'undici';
// Polyfill global environment Node.js untuk Jest
globalThis.TextEncoder = TextEncoder;
globalThis.TextDecoder = TextDecoder as typeof globalThis.TextDecoder;
globalThis.ReadableStream = ReadableStream as unknown as typeof globalThis.ReadableStream;
globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
globalThis.Headers = Headers as unknown as typeof globalThis.Headers;
globalThis.Request = Request as unknown as typeof globalThis.Request;
globalThis.Response = Response as unknown as typeof globalThis.Response;
globalThis.FormData = FormData as unknown as typeof globalThis.FormData;Konfigurasi MSW Server dan Lifecycle Hermetis
Definisikan handler default dan buat instance server menggunakan modul Node MSW.
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('https://api.example.com/profile', () => {
return HttpResponse.json({
id: 'usr_123',
name: 'Budi Pratama',
email: 'budi@example.com',
});
}),
];Inisialisasi server untuk Jest:
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);Atur lifecycle server di jest.setup.ts untuk memastikan isolasi antar-test:
// Tambahkan di jest.setup.ts setelah polyfill
import { server } from './src/mocks/server';
beforeAll(() => {
// Blokir semua network request yang tidak terdaftar di handler
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
// Reset runtime handler override agar tidak bocor ke test file lain
server.resetHandlers();
});
afterAll(() => {
server.close();
});Parameter onUnhandledRequest: 'error' memastikan pipeline CI langsung gagal jika ada kode yang mencoba memanggil endpoint tanpa mock terdefinisi, mencegah flakiness akibat timeout request eksternal.
Implementasi Komponen Target
Komponen menampilkan loading indicator, data user setelah fetch selesai, dan opsi retry jika API mengembalikan error.
// src/components/UserProfile.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator, Button } from 'react-native';
interface User {
id: string;
name: string;
}
export const UserProfile = () => {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const fetchProfile = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('https://api.example.com/profile');
if (!res.ok) throw new Error('Gagal memuat profil');
const data = await res.json();
setUser(data);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProfile();
}, []);
if (loading) return <ActivityIndicator testID="loading-indicator" />;
if (error) {
return (
<View>
<Text testID="error-message">{error}</Text>
<Button title="Coba Lagi" onPress={fetchProfile} />
</View>
);
}
return (
<View>
<Text testID="user-name">{user?.name}</Text>
</View>
);
};Pengujian Skenario Async dengan RNTL
Pengujian integrasi menggunakan React Native Testing Library (RNTL) untuk memverifikasi lifecycle komponen: status loading, rendering data sukses, runtime failure override, dan pemulihan (recovery).
// src/components/__tests__/UserProfile.test.tsx
import React from 'react';
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
import userEvent from '@testing-library/react-native';
import { http, HttpResponse } from 'msw';
import { server } from '../../mocks/server';
import { UserProfile } from '../UserProfile';
describe('UserProfile Integration Test', () => {
it('menampilkan loading lalu merender data user dengan sukses', async () => {
render(<UserProfile />);
// Verifikasi initial loading state
expect(screen.getByTestID('loading-indicator')).toBeTruthy();
// Tunggu loading hilang dan verifikasi data masuk
await waitForElementToBeRemoved(() => screen.getByTestID('loading-indicator'));
expect(screen.getByTestID('user-name')).toHaveTextContent('Budi Pratama');
});
it('menangani skenario error 500 dan berhasil memuat data setelah retry', async () => {
// Override handler khusus untuk iterasi request pertama
server.use(
http.get('https://api.example.com/profile', () => {
return new HttpResponse(null, { status: 500 });
}, { once: true })
);
const user = userEvent.setup();
render(<UserProfile />);
// Tunggu response error muncul
const errorText = await screen.findByTestID('error-message');
expect(errorText).toHaveTextContent('Gagal memuat profil');
// Klik tombol coba lagi (handler default MSW akan merespons 200 OK)
const retryBtn = screen.getByText('Coba Lagi');
await user.press(retryBtn);
// Verifikasi data berhasil tampil setelah retry
expect(await screen.findByTestID('user-name')).toHaveTextContent('Budi Pratama');
});
});Pencegahan Test Leakage dan Masalah Timer di CI
Tiga faktor utama pemicu flaky test saat menggunakan MSW di React Native:
- Handler Leakage: Penggunaan
server.use()tanpaserver.resetHandlers()diafterEachmenyebabkan override handler menular ke file test berikutnya. Pastikan pemanggilanserver.resetHandlers()diletakkan di global setup. - Konflik Fake Timers: Pemanggilan
jest.useFakeTimers()membekukan timer queue yang digunakan engine network (Fetch/Node HTTP streams). Jika menggunakan fake timers, operasikan macro/microtasks secara eksplisit viajest.runOnlyPendingTimers()atau nonaktifkan fake timer saat menangani testing network berulang. - Async Timing Races: Gunakan API
findBy*atauwaitForElementToBeRemoveddaripadawaitFordengan assertion manual di dalamnya untuk memangkas retry cycles yang tidak sinkron.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!