Pelajari cara efektif menggunakan utilitas `act` dalam pengujian React untuk memastikan komponen Anda berperilaku sesuai harapan dan menghindari jebakan umum seperti pembaruan state asinkron.
Menguasai Pengujian React dengan Utilitas `act`: Panduan Komprehensif
Pengujian adalah landasan dari perangkat lunak yang kuat dan dapat dipelihara. Dalam ekosistem React, pengujian yang menyeluruh sangat penting untuk memastikan komponen Anda berperilaku seperti yang diharapkan dan memberikan pengalaman pengguna yang andal. Utilitas `act`, yang disediakan oleh `react-dom/test-utils`, adalah alat penting untuk menulis pengujian React yang andal, terutama saat berhadapan dengan pembaruan state asinkron dan efek samping.
Apa itu Utilitas `act`?
Utilitas `act` adalah fungsi yang menyiapkan komponen React untuk asersi. Ini memastikan bahwa semua pembaruan dan efek samping terkait telah diterapkan ke DOM sebelum Anda mulai membuat asersi. Anggap saja sebagai cara untuk menyinkronkan pengujian Anda dengan state internal dan proses rendering React.
Pada intinya, `act` membungkus kode apa pun yang menyebabkan pembaruan state React terjadi. Ini termasuk:
- Penangan event (misalnya, `onClick`, `onChange`)
- Hook `useEffect`
- Setter `useState`
- Kode lain apa pun yang memodifikasi state komponen
Tanpa `act`, pengujian Anda mungkin membuat asersi sebelum React sepenuhnya memproses pembaruan, yang menyebabkan hasil yang tidak stabil dan tidak dapat diprediksi. Anda mungkin melihat peringatan seperti "Pembaruan pada [komponen] di dalam sebuah pengujian tidak dibungkus dalam act(...)." Peringatan ini menunjukkan potensi kondisi balapan di mana pengujian Anda membuat asersi sebelum React berada dalam keadaan yang konsisten.
Mengapa `act` Penting?
Alasan utama menggunakan `act` adalah untuk memastikan bahwa komponen React Anda berada dalam keadaan yang konsisten dan dapat diprediksi selama pengujian. Ini mengatasi beberapa masalah umum:
1. Mencegah Masalah Pembaruan State Asinkron
Pembaruan state React seringkali bersifat asinkron, yang berarti tidak terjadi secara langsung. Ketika Anda memanggil `setState`, React menjadwalkan pembaruan tetapi tidak langsung menerapkannya. Tanpa `act`, pengujian Anda mungkin menegaskan suatu nilai sebelum pembaruan state diproses, yang menyebabkan hasil yang salah.
Contoh: Pengujian yang Salah (Tanpa `act`)
import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
test('increments the counter', () => {
render(<Counter />);
const incrementButton = screen.getByText('Increment');
fireEvent.click(incrementButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument(); // Ini mungkin gagal!
});
Dalam contoh ini, asersi `expect(screen.getByText('Count: 1')).toBeInTheDocument();` mungkin gagal karena pembaruan state yang dipicu oleh `fireEvent.click` belum sepenuhnya diproses saat asersi dibuat.
2. Memastikan Semua Efek Samping Diproses
Hook `useEffect` sering memicu efek samping, seperti mengambil data dari API atau memperbarui DOM secara langsung. `act` memastikan bahwa efek samping ini selesai sebelum pengujian berlanjut, mencegah kondisi balapan dan memastikan bahwa komponen Anda berperilaku seperti yang diharapkan.
3. Meningkatkan Keandalan dan Prediktabilitas Pengujian
Dengan menyinkronkan pengujian Anda dengan proses internal React, `act` membuat pengujian Anda lebih andal dan dapat diprediksi. Ini mengurangi kemungkinan pengujian yang tidak stabil yang terkadang lolos dan terkadang gagal, membuat rangkaian pengujian Anda lebih dapat dipercaya.
Cara Menggunakan Utilitas `act`
Utilitas `act` mudah digunakan. Cukup bungkus kode apa pun yang menyebabkan pembaruan state React atau efek samping dalam panggilan `act`.
Contoh: Pengujian yang Benar (Dengan `act`)
import React, { useState } from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
test('increments the counter', async () => {
render(<Counter />);
const incrementButton = screen.getByText('Increment');
await act(async () => {
fireEvent.click(incrementButton);
});
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Dalam contoh yang diperbaiki ini, panggilan `fireEvent.click` dibungkus dalam panggilan `act`. Ini memastikan bahwa React telah sepenuhnya memproses pembaruan state sebelum asersi dibuat.
`act` Asinkron
Utilitas `act` dapat digunakan secara sinkron atau asinkron. Saat berhadapan dengan kode asinkron (misalnya, hook `useEffect` yang mengambil data), Anda harus menggunakan versi asinkron dari `act`.
Contoh: Menguji Efek Samping Asinkron
import React, { useState, useEffect } from 'react';
import { render, screen, act } from '@testing-library/react';
async function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('Fetched Data');
}, 50);
});
}
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
async function loadData() {
const result = await fetchData();
setData(result);
}
loadData();
}, []);
return <div>{data ? <p>{data}</p> : <p>Loading...</p>}</div>;
}
test('fetches data correctly', async () => {
render(<MyComponent />);
// Render awal menampilkan "Loading..."
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Tunggu data dimuat dan komponen diperbarui
await act(async () => {
// Fungsi fetchData akan resolve setelah 50ms, memicu pembaruan state.
// Await di sini memastikan kita menunggu act menyelesaikan semua pembaruan.
await new Promise(resolve => setTimeout(resolve, 0)); // Jeda singkat untuk memungkinkan act memproses.
});
// Pastikan bahwa data ditampilkan
expect(screen.getByText('Fetched Data')).toBeInTheDocument();
});
Dalam contoh ini, hook `useEffect` mengambil data secara asinkron. Panggilan `act` digunakan untuk membungkus kode asinkron, memastikan bahwa komponen telah sepenuhnya diperbarui sebelum asersi dibuat. Baris `await new Promise` diperlukan untuk memberi `act` waktu untuk memproses pembaruan yang dipicu oleh panggilan `setData` di dalam hook `useEffect`, terutama di lingkungan di mana penjadwal mungkin menunda pembaruan.
Praktik Terbaik Menggunakan `act`
Untuk mendapatkan hasil maksimal dari utilitas `act`, ikuti praktik terbaik berikut:
1. Bungkus Semua Pembaruan State
Pastikan bahwa semua kode yang menyebabkan pembaruan state React dibungkus dalam panggilan `act`. Ini termasuk penangan event, hook `useEffect`, dan setter `useState`.
2. Gunakan `act` Asinkron untuk Kode Asinkron
Saat berhadapan dengan kode asinkron, gunakan versi asinkron dari `act` untuk memastikan bahwa semua efek samping selesai sebelum pengujian berlanjut.
3. Hindari Panggilan `act` Bersarang
Hindari membuat panggilan `act` bersarang. Panggilan bersarang dapat menyebabkan perilaku tak terduga dan membuat pengujian Anda lebih sulit untuk di-debug. Jika Anda perlu melakukan beberapa tindakan, bungkus semuanya dalam satu panggilan `act` tunggal.
4. Gunakan `await` dengan `act` Asinkron
Saat menggunakan versi asinkron dari `act`, selalu gunakan `await` untuk memastikan bahwa panggilan `act` telah selesai sebelum pengujian berlanjut. Ini sangat penting saat berhadapan dengan efek samping asinkron.
5. Hindari Pembungkusan Berlebihan
Meskipun penting untuk membungkus pembaruan state, hindari membungkus kode yang tidak secara langsung menyebabkan perubahan state atau efek samping. Pembungkusan berlebihan dapat membuat pengujian Anda lebih kompleks dan kurang mudah dibaca.
6. Memahami `flushMicrotasks` dan `advanceTimersByTime`
Dalam skenario tertentu, terutama ketika berhadapan dengan timer atau promise yang di-mock, Anda mungkin perlu menggunakan `act(() => jest.advanceTimersByTime(time))` atau `act(() => flushMicrotasks())` untuk memaksa React memproses pembaruan secara langsung. Ini adalah teknik yang lebih canggih, tetapi memahaminya dapat membantu untuk skenario asinkron yang kompleks.
7. Pertimbangkan Menggunakan `userEvent` dari `@testing-library/user-event`
Daripada `fireEvent`, pertimbangkan untuk menggunakan `userEvent` dari `@testing-library/user-event`. `userEvent` mensimulasikan interaksi pengguna nyata dengan lebih akurat, seringkali menangani panggilan `act` secara internal, yang mengarah ke pengujian yang lebih bersih dan lebih andal. Sebagai contoh:
import React, { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function MyComponent() {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<input type="text" value={value} onChange={handleChange} />
);
}
test('updates the input value', async () => {
render(<MyComponent />);
const inputElement = screen.getByRole('textbox');
await userEvent.type(inputElement, 'hello');
expect(inputElement.value).toBe('hello');
});
Dalam contoh ini, `userEvent.type` menangani panggilan `act` yang diperlukan secara internal, membuat pengujian lebih bersih dan lebih mudah dibaca.
Jebakan Umum dan Cara Menghindarinya
Meskipun utilitas `act` adalah alat yang ampuh, penting untuk menyadari jebakan umum dan cara menghindarinya:
1. Lupa Membungkus Pembaruan State
Jebakan yang paling umum adalah lupa membungkus pembaruan state dalam panggilan `act`. Ini dapat menyebabkan pengujian yang tidak stabil dan perilaku yang tidak dapat diprediksi. Selalu periksa kembali bahwa semua kode yang menyebabkan pembaruan state dibungkus dalam `act`.
2. Penggunaan `act` Asinkron yang Salah
Saat menggunakan versi asinkron dari `act`, penting untuk melakukan `await` pada panggilan `act`. Kegagalan untuk melakukannya dapat menyebabkan kondisi balapan dan hasil yang salah.
3. Terlalu Bergantung pada `setTimeout` atau `flushPromises`
Meskipun `setTimeout` atau `flushPromises` terkadang dapat digunakan untuk mengatasi masalah dengan pembaruan state asinkron, keduanya harus digunakan dengan hemat. Dalam kebanyakan kasus, menggunakan `act` dengan benar adalah cara terbaik untuk memastikan bahwa pengujian Anda andal.
4. Mengabaikan Peringatan
Jika Anda melihat peringatan seperti "Pembaruan pada [komponen] di dalam sebuah pengujian tidak dibungkus dalam act(...).", jangan abaikan! Peringatan ini menunjukkan potensi kondisi balapan yang perlu ditangani.
Contoh di Berbagai Kerangka Kerja Pengujian
Utilitas `act` terutama terkait dengan utilitas pengujian React, tetapi prinsipnya berlaku terlepas dari kerangka kerja pengujian spesifik yang Anda gunakan.
1. Menggunakan `act` dengan Jest dan React Testing Library
Ini adalah skenario yang paling umum. React Testing Library mendorong penggunaan `act` untuk memastikan pembaruan state yang tepat.
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
// Komponen dan pengujian (seperti yang ditunjukkan sebelumnya)
2. Menggunakan `act` dengan Enzyme
Enzyme adalah pustaka pengujian React populer lainnya, meskipun menjadi kurang umum seiring dengan meningkatnya popularitas React Testing Library. Anda masih dapat menggunakan `act` dengan Enzyme untuk memastikan pembaruan state yang tepat.
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
// Contoh komponen (mis., Counter dari contoh sebelumnya)
it('increments the counter', () => {
const wrapper = mount(<Counter />);
const button = wrapper.find('button');
act(() => {
button.simulate('click');
});
wrapper.update(); // Paksa render ulang
expect(wrapper.find('p').text()).toEqual('Count: 1');
});
Catatan: Dengan Enzyme, Anda mungkin perlu memanggil `wrapper.update()` untuk memaksa render ulang setelah panggilan `act`.
`act` dalam Konteks Global yang Berbeda
Prinsip-prinsip penggunaan `act` bersifat universal, tetapi aplikasi praktisnya mungkin sedikit berbeda tergantung pada lingkungan dan peralatan spesifik yang digunakan oleh tim pengembangan yang berbeda di seluruh dunia. Sebagai contoh:
- Tim yang menggunakan TypeScript: Tipe yang disediakan oleh `@types/react-dom` membantu memastikan bahwa `act` digunakan dengan benar dan memberikan pemeriksaan waktu kompilasi untuk potensi masalah.
- Tim yang menggunakan pipeline CI/CD: Penggunaan `act` yang konsisten memastikan bahwa pengujian dapat diandalkan dan mencegah kegagalan palsu di lingkungan CI/CD, terlepas dari penyedia infrastruktur (misalnya, GitHub Actions, GitLab CI, Jenkins).
- Tim yang bekerja dengan internasionalisasi (i18n): Saat menguji komponen yang menampilkan konten yang dilokalkan, penting untuk memastikan bahwa `act` digunakan dengan benar untuk menangani pembaruan asinkron atau efek samping yang terkait dengan pemuatan atau pembaruan string yang dilokalkan.
Kesimpulan
Utilitas `act` adalah alat vital untuk menulis pengujian React yang andal dan dapat diprediksi. Dengan memastikan bahwa pengujian Anda disinkronkan dengan proses internal React, `act` membantu mencegah kondisi balapan dan memastikan bahwa komponen Anda berperilaku seperti yang diharapkan. Dengan mengikuti praktik terbaik yang diuraikan dalam panduan ini, Anda dapat menguasai utilitas `act` dan menulis aplikasi React yang lebih kuat dan dapat dipelihara. Mengabaikan peringatan dan melewatkan penggunaan `act` menciptakan rangkaian pengujian yang membohongi pengembang dan pemangku kepentingan yang mengarah pada bug dalam produksi. Selalu gunakan `act` untuk membuat pengujian yang dapat dipercaya.