Kuasai React Testing Library (RTL) dengan panduan lengkap ini. Pelajari cara menulis pengujian yang efektif, mudah dipelihara, dan berpusat pada pengguna untuk aplikasi React Anda, dengan fokus pada praktik terbaik dan contoh dunia nyata.
React Testing Library: Panduan Komprehensif
Dalam lanskap pengembangan web yang serba cepat saat ini, memastikan kualitas dan keandalan aplikasi React Anda adalah hal yang terpenting. React Testing Library (RTL) telah muncul sebagai solusi populer dan efektif untuk menulis pengujian yang berfokus pada perspektif pengguna. Panduan ini memberikan gambaran lengkap tentang RTL, mencakup segalanya mulai dari konsep dasar hingga teknik-teknik canggih, memberdayakan Anda untuk membangun aplikasi React yang kuat dan mudah dipelihara.
Mengapa Memilih React Testing Library?
Pendekatan pengujian tradisional sering kali bergantung pada detail implementasi, membuat pengujian menjadi rapuh dan rentan rusak dengan perubahan kode kecil. RTL, di sisi lain, mendorong Anda untuk menguji komponen Anda seolah-olah pengguna berinteraksi dengannya, berfokus pada apa yang dilihat dan dialami pengguna. Pendekatan ini menawarkan beberapa keuntungan utama:
- Pengujian Berpusat pada Pengguna: RTL mempromosikan penulisan pengujian yang mencerminkan perspektif pengguna, memastikan bahwa aplikasi Anda berfungsi seperti yang diharapkan dari sudut pandang pengguna akhir.
- Mengurangi Kerapuhan Pengujian: Dengan menghindari pengujian detail implementasi, pengujian RTL lebih kecil kemungkinannya untuk rusak saat Anda melakukan refactor pada kode Anda, menghasilkan pengujian yang lebih mudah dipelihara dan kuat.
- Desain Kode yang Lebih Baik: RTL mendorong Anda untuk menulis komponen yang dapat diakses dan mudah digunakan, yang mengarah pada desain kode yang lebih baik secara keseluruhan.
- Fokus pada Aksesibilitas: RTL mempermudah pengujian aksesibilitas komponen Anda, memastikan bahwa aplikasi Anda dapat digunakan oleh semua orang.
- Proses Pengujian yang Disederhanakan: RTL menyediakan API yang sederhana dan intuitif, membuatnya lebih mudah untuk menulis dan memelihara pengujian.
Menyiapkan Lingkungan Pengujian Anda
Sebelum Anda dapat mulai menggunakan RTL, Anda perlu menyiapkan lingkungan pengujian Anda. Ini biasanya melibatkan instalasi dependensi yang diperlukan dan mengonfigurasi kerangka kerja pengujian Anda.
Prasyarat
- Node.js dan npm (atau yarn): Pastikan Anda telah menginstal Node.js dan npm (atau yarn) di sistem Anda. Anda dapat mengunduhnya dari situs web resmi Node.js.
- Proyek React: Anda harus memiliki proyek React yang sudah ada atau membuat yang baru menggunakan Create React App atau alat serupa.
Instalasi
Instal paket-paket berikut menggunakan npm atau yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Atau, menggunakan yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Penjelasan Paket:
- @testing-library/react: Pustaka inti untuk menguji komponen React.
- @testing-library/jest-dom: Menyediakan pencocok Jest kustom untuk melakukan asersi tentang node DOM.
- Jest: Kerangka kerja pengujian JavaScript yang populer.
- babel-jest: Transformer Jest yang menggunakan Babel untuk mengompilasi kode Anda.
- @babel/preset-env: Preset Babel yang menentukan plugin dan preset Babel yang diperlukan untuk mendukung lingkungan target Anda.
- @babel/preset-react: Preset Babel untuk React.
Konfigurasi
Buat file `babel.config.js` di direktori root proyek Anda dengan konten berikut:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Perbarui file `package.json` Anda untuk menyertakan skrip pengujian:
{
"scripts": {
"test": "jest"
}
}
Buat file `jest.config.js` di direktori root proyek Anda untuk mengonfigurasi Jest. Konfigurasi minimal mungkin terlihat seperti ini:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Buat file `src/setupTests.js` dengan konten berikut. Ini memastikan bahwa pencocok Jest DOM tersedia di semua pengujian Anda:
import '@testing-library/jest-dom/extend-expect';
Menulis Tes Pertama Anda
Mari kita mulai dengan contoh sederhana. Misalkan Anda memiliki komponen React yang menampilkan pesan sapaan:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Sekarang, mari kita tulis tes untuk komponen ini:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('merender pesan sapaan', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Penjelasan:
- `render`: Fungsi ini merender komponen ke dalam DOM.
- `screen`: Objek ini menyediakan metode untuk melakukan kueri pada DOM.
- `getByText`: Metode ini menemukan elemen berdasarkan konten teksnya. Bendera `/i` membuat pencarian tidak peka huruf besar-kecil.
- `expect`: Fungsi ini digunakan untuk membuat asersi tentang perilaku komponen.
- `toBeInTheDocument`: Pencocok ini menegaskan bahwa elemen tersebut ada di dalam DOM.
Untuk menjalankan tes, jalankan perintah berikut di terminal Anda:
npm test
Jika semuanya dikonfigurasi dengan benar, tes seharusnya berhasil.
Kueri RTL yang Umum
RTL menyediakan berbagai metode kueri untuk menemukan elemen di dalam DOM. Kueri-kueri ini dirancang untuk meniru bagaimana pengguna berinteraksi dengan aplikasi Anda.
`getByRole`
Kueri ini menemukan elemen berdasarkan peran ARIA-nya. Merupakan praktik yang baik untuk menggunakan `getByRole` kapan pun memungkinkan, karena ini mempromosikan aksesibilitas dan memastikan bahwa tes Anda tangguh terhadap perubahan dalam struktur DOM yang mendasarinya.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Kueri ini menemukan elemen berdasarkan teks dari label yang terkait. Ini berguna untuk menguji elemen formulir.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Kueri ini menemukan elemen berdasarkan teks placeholder-nya.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Kueri ini menemukan elemen gambar berdasarkan teks alt-nya. Penting untuk menyediakan teks alt yang bermakna untuk semua gambar untuk memastikan aksesibilitas.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Kueri ini menemukan elemen berdasarkan atribut judulnya.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Kueri ini menemukan elemen berdasarkan nilai tampilannya. Ini berguna untuk menguji input formulir dengan nilai yang sudah diisi sebelumnya.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
Kueri `getAllBy*`
Selain kueri `getBy*`, RTL juga menyediakan kueri `getAllBy*`, yang mengembalikan array elemen yang cocok. Ini berguna ketika Anda perlu menegaskan bahwa beberapa elemen dengan karakteristik yang sama ada di dalam DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
Kueri `queryBy*`
Kueri `queryBy*` mirip dengan kueri `getBy*`, tetapi mereka mengembalikan `null` jika tidak ada elemen yang cocok yang ditemukan, alih-alih melempar kesalahan. Ini berguna ketika Anda ingin menegaskan bahwa sebuah elemen *tidak* ada di dalam DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
Kueri `findBy*`
Kueri `findBy*` adalah versi asinkron dari kueri `getBy*`. Mereka mengembalikan Promise yang diselesaikan ketika elemen yang cocok ditemukan. Ini berguna untuk menguji operasi asinkron, seperti mengambil data dari API.
// Mensimulasikan pengambilan data asinkron
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('memuat data secara asinkron', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Mensimulasikan Interaksi Pengguna
RTL menyediakan API `fireEvent` dan `userEvent` untuk mensimulasikan interaksi pengguna, seperti mengklik tombol, mengetik di bidang input, dan mengirimkan formulir.
`fireEvent`
`fireEvent` memungkinkan Anda untuk memicu event DOM secara terprogram. Ini adalah API tingkat rendah yang memberi Anda kontrol terperinci atas event yang dipicu.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('mensimulasikan klik tombol', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` adalah API tingkat tinggi yang mensimulasikan interaksi pengguna dengan lebih realistis. Ini menangani detail seperti manajemen fokus dan urutan event, membuat tes Anda lebih kuat dan tidak rapuh.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('mensimulasikan pengetikan di bidang input', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Menguji Kode Asinkron
Banyak aplikasi React melibatkan operasi asinkron, seperti mengambil data dari API. RTL menyediakan beberapa alat untuk menguji kode asinkron.
`waitFor`
`waitFor` memungkinkan Anda menunggu hingga suatu kondisi menjadi benar sebelum membuat asersi. Ini berguna untuk menguji operasi asinkron yang membutuhkan waktu untuk selesai.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('menunggu data dimuat', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
Kueri `findBy*`
Seperti yang disebutkan sebelumnya, kueri `findBy*` bersifat asinkron dan mengembalikan Promise yang diselesaikan ketika elemen yang cocok ditemukan. Ini berguna untuk menguji operasi asinkron yang menghasilkan perubahan pada DOM.
Menguji Hooks
React Hooks adalah fungsi yang dapat digunakan kembali yang mengenkapsulasi logika stateful. RTL menyediakan utilitas `renderHook` dari `@testing-library/react-hooks` (yang sudah usang dan digantikan langsung oleh `@testing-library/react` per v17) untuk menguji Hooks kustom secara terisolasi.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('menambah penghitung', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Penjelasan:
- `renderHook`: Fungsi ini merender Hook dan mengembalikan sebuah objek yang berisi hasil dari Hook tersebut.
- `act`: Fungsi ini digunakan untuk membungkus kode apa pun yang menyebabkan pembaruan state. Ini memastikan bahwa React dapat mengelompokkan dan memproses pembaruan dengan benar.
Teknik Pengujian Tingkat Lanjut
Setelah Anda menguasai dasar-dasar RTL, Anda dapat menjelajahi teknik pengujian yang lebih canggih untuk meningkatkan kualitas dan kemudahan pemeliharaan tes Anda.
Melakukan Mocking pada Modul
Terkadang, Anda mungkin perlu melakukan mock pada modul atau dependensi eksternal untuk mengisolasi komponen Anda dan mengontrol perilakunya selama pengujian. Jest menyediakan API mocking yang kuat untuk tujuan ini.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('mengambil data dari API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Penjelasan:
- `jest.mock('../api/dataService')`: Baris ini melakukan mock pada modul `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Baris ini mengonfigurasi fungsi `fetchData` yang di-mock untuk mengembalikan Promise yang diselesaikan dengan data yang ditentukan.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Baris ini menegaskan bahwa fungsi `fetchData` yang di-mock dipanggil satu kali.
Penyedia Konteks (Context Providers)
Jika komponen Anda bergantung pada Penyedia Konteks (Context Provider), Anda perlu membungkus komponen Anda di dalam provider tersebut selama pengujian. Ini memastikan bahwa komponen memiliki akses ke nilai-nilai konteks.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('mengganti tema', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
Penjelasan:
- Kami membungkus `MyComponent` dalam `ThemeProvider` untuk menyediakan konteks yang diperlukan selama pengujian.
Pengujian dengan Router
Saat menguji komponen yang menggunakan React Router, Anda perlu menyediakan konteks Router tiruan (mock). Anda dapat mencapainya dengan menggunakan komponen `MemoryRouter` dari `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('merender tautan ke halaman tentang', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Penjelasan:
- Kami membungkus `MyComponent` dalam `MemoryRouter` untuk menyediakan konteks Router tiruan.
- Kami menegaskan bahwa elemen tautan memiliki atribut `href` yang benar.
Praktik Terbaik untuk Menulis Tes yang Efektif
Berikut adalah beberapa praktik terbaik yang harus diikuti saat menulis tes dengan RTL:
- Fokus pada Interaksi Pengguna: Tulis tes yang mensimulasikan bagaimana pengguna berinteraksi dengan aplikasi Anda.
- Hindari Menguji Detail Implementasi: Jangan menguji cara kerja internal komponen Anda. Sebaliknya, fokuslah pada perilaku yang dapat diamati.
- Tulis Tes yang Jelas dan Ringkas: Buat tes Anda mudah dipahami dan dipelihara.
- Gunakan Nama Tes yang Bermakna: Pilih nama tes yang secara akurat mendeskripsikan perilaku yang sedang diuji.
- Jaga Agar Tes Tetap Terisolasi: Hindari dependensi antar tes. Setiap tes harus independen dan mandiri.
- Uji Kasus-kasus Tepi (Edge Cases): Jangan hanya menguji jalur yang bahagia (happy path). Pastikan untuk menguji kasus-kasus tepi dan kondisi kesalahan juga.
- Tulis Tes Sebelum Anda Menulis Kode: Pertimbangkan untuk menggunakan Test-Driven Development (TDD) untuk menulis tes sebelum Anda menulis kode Anda.
- Ikuti Pola "AAA": Arrange, Act, Assert. Pola ini membantu menyusun tes Anda dan membuatnya lebih mudah dibaca.
- Jaga agar tes Anda cepat: Tes yang lambat dapat membuat pengembang enggan menjalankannya secara sering. Optimalkan kecepatan tes Anda dengan melakukan mock pada permintaan jaringan dan meminimalkan jumlah manipulasi DOM.
- Gunakan pesan kesalahan yang deskriptif: Ketika asersi gagal, pesan kesalahan harus memberikan informasi yang cukup untuk mengidentifikasi penyebab kegagalan dengan cepat.
Kesimpulan
React Testing Library adalah alat yang ampuh untuk menulis tes yang efektif, mudah dipelihara, dan berpusat pada pengguna untuk aplikasi React Anda. Dengan mengikuti prinsip dan teknik yang diuraikan dalam panduan ini, Anda dapat membangun aplikasi yang kuat dan andal yang memenuhi kebutuhan pengguna Anda. Ingatlah untuk fokus pada pengujian dari perspektif pengguna, hindari menguji detail implementasi, dan tulis tes yang jelas dan ringkas. Dengan merangkul RTL dan mengadopsi praktik terbaik, Anda dapat secara signifikan meningkatkan kualitas dan kemudahan pemeliharaan proyek React Anda, terlepas dari lokasi Anda atau persyaratan spesifik dari audiens global Anda.