Test adalah kode kecil yang memakai komponenmu seperti user memakainya, lalu mengecek hasilnya, otomatis, setiap kali kodenya berubah. Di React, pasangan yang paling umum: Vitest sebagai test runner (yang menjalankan test dan melaporkan lulus atau gagal) dan React Testing Library (RTL) untuk merender komponen dan mencari elemen di dalamnya. Bayangin petugas QC di pabrik roti. Dia gak peduli mesin di dalam pabrik bekerja bagaimana. Dia ambil satu roti, cek bentuknya, pencet, cicipi, lalu bilang lulus atau gagal. Testing Library juga begitu: dia gak membaca state di dalam komponen, dia mengecek apa yang terlihat dan bisa dilakukan user.
🧪 Tebakan cepat: kamu mau mencari tombol Tambah di dalam test. Mana yang paling disarankan:
getByTestId("tambah"),getByText("Tambah"), ataugetByRole("button", { name: "Tambah" })? Tebak dulu, jawabannya ada di bagian "Cari elemen seperti user mencarinya".
Test pertama: render, aksi, cek
Komponen yang dites:
// Counter.jsx
import { useState } from "react"
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Tambah</button>
</div>
)
}
Test-nya:
// Counter.test.jsx
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { test, expect } from "vitest"
import Counter from "./Counter"
test("count bertambah saat tombol Tambah diklik", async () => {
const user = userEvent.setup()
render(<Counter />)
expect(screen.getByText("Count: 0")).toBeInTheDocument()
await user.click(screen.getByRole("button", { name: "Tambah" }))
expect(screen.getByText("Count: 1")).toBeInTheDocument()
})
test("...", async () => { ... })adalah satu test: namanya menjelaskan perilaku yang dicek, isinya langkah-langkahnya.render(<Counter />)merender komponen ke DOM tiruan (jsdom) di dalam test, tanpa membuka browser.screenadalah seluruh halaman yang sudah dirender.screen.getByText("Count: 0")mencari elemen yang teksnya persisCount: 0.userEvent.setup()laluawait user.click(...)meniru klik user beneran: pointer, fokus, lalu klik. Klik-nya async, jadi wajibawait.expect(...).toBeInTheDocument()disebut assertion: memastikan elemen itu ada di halaman. Kalau gak terpenuhi,expectmelempar error, dan test-nya gagal.
Jalankan dengan npx vitest run. Kalau lulus, terminal menampilkan:
✓ src/Counter.test.jsx (1 test) 48ms
Test Files 1 passed (1)
Tests 1 passed (1)
Kalau tombolnya rusak dan angkanya gak naik, getByText("Count: 1") gak menemukan apa-apa dan melempar error:
FAIL src/Counter.test.jsx > count bertambah saat tombol Tambah diklik
TestingLibraryElementError: Unable to find an element with the text: Count: 1. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Cari elemen seperti user mencarinya
Testing Library punya urutan query yang disarankan, dari yang paling dekat dengan cara user (dan screen reader) menemukan elemen:
getByRole("button", { name: "Tambah" }): berdasarkan role, yaitu peran elemen (button, heading, textbox, checkbox), dan namanya.getByLabelText("Email"): input berdasarkan label-nya.getByPlaceholderText("Cari..."): input berdasarkan placeholder.getByText("Count: 0"): elemen biasa berdasarkan teksnya.getByDisplayValue,getByAltText(gambar),getByTitle.getByTestId("tambah"): berdasarkan atributdata-testid. Pilihan terakhir, karena user gak pernah melihatnya.
Jadi jawabannya getByRole. Itu jawaban tebakannya. Bedanya kelihatan waktu tombol Tambah diubah, dan ketiga versinya tetap membawa data-testid="tambah":
| Tombolnya diubah jadi | getByRole |
getByText("Tambah") |
getByTestId |
|---|---|---|---|
<button aria-label="Tambah">+</button> (ikon) |
lulus | gagal | lulus |
<div onClick={...}>Tambah</div> |
gagal | lulus | lulus |
Versi ikon masih tombol yang sama bagi user, dan cuma getByText yang salah menganggapnya rusak. Versi div benar-benar rusak, karena gak bisa ditekan lewat keyboard dan gak dikenali screen reader sebagai tombol, dan cuma getByRole yang menangkapnya.
Awalan query-nya juga punya arti:
getBy...melempar error kalau elemennya gak ada.queryBy...mengembalikannull, buat mengecek sesuatu yang gak boleh ada:expect(screen.queryByText("Gagal")).not.toBeInTheDocument().findBy...menunggu (secara default sampai 1 detik) elemen yang muncul belakangan, misalnya sesudah fetch:await screen.findByText("Budi").
Di balik expect
expect sendiri gak ajaib. Ini versi mininya:
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) {
throw new Error(`Expected ${expected}, received ${actual}`)
}
},
}
}
expect(1 + 1).toBe(2) // diam saja: lulus
expect(1 + 1).toBe(3) // melempar error: test gagal
expect(actual)mengembalikan object berisi method yang disebut matcher. Tiap matcher masih ingatactual, karena dibuat di dalamexpect(ini closure dari track JavaScript).- Matcher asli melempar error saat gagal, dan test runner menangkap error itu lalu menandai test-nya gagal.
- Di latihan kamu bikin versi mini
buatAssertion(teks). Supaya gampang dicek, method-nya mengembalikantrueataufalse, bukan melempar error:buatAssertion("halo dunia").toContain("dunia")menghasilkantrue, danbuatAssertion("").toBeInTheDocument()menghasilkanfalse.
Memasang alatnya
npm install -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom
Lalu di vite.config.js, tambahkan test: { environment: "jsdom", setupFiles: "./src/setupTests.js" }, dan isi src/setupTests.js dengan satu baris import "@testing-library/jest-dom/vitest". Baris itu yang menambahkan matcher seperti toBeInTheDocument.
⚠️ Jebakan yang sering ketemu
- Lupa
awaitdiuser.click. Assertion-nya jalan sebelum klik selesai, jadiCount: 1belum ada dan test-nya gagal. getByTestIddi mana-mana. Test tetap lulus walau tombolnya berubah jadidivyang gak bisa dipakai keyboard. Mulai darigetByRole.- Mengetes isi state. Mengecek nilai
countdi dalam komponen, atau berapa kali komponennya render, bikin test pecah waktu kodenya dirapikan walau tampilannya sama. Cek yang terlihat user. getByuntuk sesuatu yang gak boleh ada.getBymelempar error duluan. PakaiqueryBy.
TL;DR: Test memakai komponen seperti user, lalu mengecek hasilnya. Pola dasarnya: render(<Komponen />), cari elemen lewat screen, lakukan aksi dengan await user.click(...), lalu cek dengan expect(...).toBeInTheDocument(). Vitest menjalankan test-nya, React Testing Library merender dan mencari elemen. Urutan query: getByRole dulu, lalu getByLabelText, getByPlaceholderText, getByText, dan getByTestId paling akhir. queryBy untuk yang gak boleh ada, findBy untuk yang muncul belakangan. expect(x) mengembalikan object matcher yang ingat x dan melempar error kalau cek-nya gagal.