Component testing menguji satu komponen UI (misalnya komponen React) sendirian, tanpa menjalankan seluruh aplikasi: komponennya di-render ke DOM tiruan, lalu test-nya mengetik dan mengklik seperti user, dan mengecek apa yang tampil. Bayangin menguji satu tombol lift di bengkel sebelum dipasang di gedung. Kamu menekannya persis seperti penumpang, lalu melihat apakah lampunya menyala. Kamu gak membongkar kabel di dalamnya.
🧪 Tebakan cepat: tombol
<button>Kirim</button>diganti<div onClick={...}>Kirim</div>yang tampilannya sama persis. Test yang mencari dengangetByText("Kirim")tetap lulus. Test yang mencari dengangetByRole("button", { name: "Kirim" })lulus atau gagal? Jawabannya ada di bagian "Cari elemen seperti user mencarinya".
Render, lalu cari lewat screen
Alatnya: Vitest sebagai test runner, jsdom sebagai DOM tiruan (browser palsu di dalam Node), dan Testing Library untuk me-render dan mencari elemen.
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
Di vitest.config.js, tulis test: { environment: "jsdom", setupFiles: ["./src/setup.js"] }, dan isi src/setup.js dengan satu baris: import "@testing-library/jest-dom/vitest";. Baris itu menambah matcher DOM seperti toBeInTheDocument().
Contoh di pelajaran ini dan di Lab: form langganan dengan kolom Email dan tombol Kirim.
// FormLangganan.jsx
import { useState } from "react";
export function FormLangganan() {
const [email, setEmail] = useState("");
const [terkirim, setTerkirim] = useState(false);
return (
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="button" onClick={() => setTerkirim(true)}>Kirim</button>
{terkirim && <p>{`Terima kasih, ${email}!`}</p>}
</div>
);
}
// FormLangganan.test.jsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { FormLangganan } from "./FormLangganan";
describe("FormLangganan", () => {
it("menampilkan ucapan terima kasih setelah email dikirim", async () => {
const user = userEvent.setup();
render(<FormLangganan />);
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.click(screen.getByRole("button", { name: "Kirim" }));
expect(screen.getByText("Terima kasih, [email protected]!")).toBeInTheDocument();
});
});
render(<FormLangganan />)memasang komponen ke DOM tiruan.screenadalah seluruh halaman uji. Semua pencarian dimulai dari sini.getByLabelText("Email")mencari kolom input lewat label-nya, seperti user membaca label.user.type(...)mengetik huruf per huruf,user.click(...)mengklik. Keduanyaawait, karena user-event berjalan async.toBeInTheDocument()lulus kalau elemennya ada di halaman.
Test ini gak menyentuh email atau terkirim di dalam komponen. Yang dicek cuma yang dilihat user. Kalau isi komponennya ditulis ulang tapi tampilannya sama, test-nya tetap lulus.
Cari elemen seperti user mencarinya
Testing Library punya urutan prioritas query. Makin atas, makin mirip cara user (dan screen reader) menemukan elemen:
getByRole: lewat peran aksesibilitas (button,textbox,link,heading,checkbox) dan namanya. Coba ini dulu.getByLabelText: untuk kolom form yang punya label.getByPlaceholderText: kalau gak ada label (sebaiknya ada).getByText: untuk teks biasa, paragraf, pesan.getByDisplayValue,getByAltText,getByTitle: untuk kasus khusus.getByTestId: jalan terakhir, lewat atributdata-testid.
{ name: "Kirim" } di getByRole adalah nama yang dibacakan screen reader: teks di dalam tombol, label, atau aria-label.
Sekarang tebakannya. <div onClick> gak punya peran button: gak bisa difokus dengan Tab, gak bisa ditekan dengan Enter, dan screen reader gak mengenalinya sebagai tombol. Jadi getByRole gagal, dengan pesan:
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "Kirim"
Here are the accessible roles:
textbox:
Name "Email":
<input
id="email"
type="email"
value="[email protected]"
/>
Itu jawaban tebakannya. getByText("Kirim") tetap lulus karena teksnya ada, jadi test hijau padahal tombolnya rusak untuk pengguna keyboard. getByRole menguji aksesibilitas sekalian.
getBy, queryBy, findBy
Tiap query punya tiga bentuk, dan bedanya ada di apa yang terjadi kalau elemennya gak ketemu:
| Bentuk | Kalau gak ketemu | Async? | Dipakai untuk |
|---|---|---|---|
getBy... |
melempar error | tidak | elemen yang harus ada sekarang |
queryBy... |
mengembalikan null |
tidak | memastikan elemen gak ada |
findBy... |
mencoba ulang sampai batas waktu (1 detik), lalu error | ya, pakai await |
elemen yang muncul belakangan |
// Sebelum tombol diklik, ucapan terima kasih belum ada
expect(screen.queryByText("Terima kasih, [email protected]!")).toBeNull();
// Elemen yang muncul setelah fetch selesai
const nama = await screen.findByText("Budi Santoso");
getByText untuk elemen yang belum ada langsung melempar error, jadi test-nya berhenti di baris itu. Untuk memastikan sesuatu gak tampil, pakai queryBy.
userEvent atau fireEvent
Ada dua cara memicu interaksi:
fireEventmengirim satu event DOM.fireEvent.change(input, { target: { value: "[email protected]" } })cuma mengirimchangedengan nilai yang langsung jadi.userEventmengirim rangkaian event yang terjadi saat orang sungguhan memakai mouse dan keyboard.user.clickmengirimpointerdown,mousedown,focus,pointerup,mouseup, laluclick(dan beberapa event hover sebelumnya).user.typemengklik kolomnya dulu, lalu untuk tiap huruf mengirimkeydown,keypress,beforeinput,input,keyup.
Bedanya kelihatan waktu ada bug. Misalnya kolom Email gak sengaja diberi maxLength={5}. User sungguhan cuma bisa mengetik budi@. user.type juga berhenti di budi@, jadi test-nya gagal, dan itu benar. fireEvent.change menempel [email protected] langsung tanpa peduli batasnya, jadi test-nya lulus dan bug-nya lolos. Karena itu userEvent lebih direkomendasikan: interaksinya lebih realistis.
⚠️ Jebakan yang sering ketemu
- Semua lewat
getByTestId. Test-nya lulus walau tombolnya cuma<div>. PakaigetByRoledulu. getByuntuk mengecek elemen gak ada.getBymelempar error sebelumexpectsempat jalan. PakaiqueryBylalutoBeNull()ataunot.toBeInTheDocument().- Lupa
await.user.type,user.click, danfindBysemuanya async. Tanpaawait,expectjalan sebelum interaksinya selesai. - Mengecek state di dalam komponen. Test yang membaca state ikut patah waktu komponennya ditulis ulang. Cek yang tampil di layar.
TL;DR: Component testing me-render satu komponen dengan render() ke DOM tiruan (jsdom), lalu mencari elemen lewat screen seperti user mencarinya: getByRole dulu (peran dan nama, sekalian menguji aksesibilitas), lalu getByLabelText, getByText, dan getByTestId paling akhir. getBy melempar error kalau gak ketemu, queryBy mengembalikan null (untuk memastikan elemen gak ada), findBy menunggu elemen yang muncul belakangan. userEvent lebih direkomendasikan daripada fireEvent karena mensimulasikan rangkaian event seperti user sungguhan, jadi bug seperti maxLength ikut ketahuan.