Pas project React tumbuh, refactor jadi tegang — "kalo aku ubah component A, apakah komponen B yang depend masih jalan?". Manual click test berjam-jam tiap commit gak scalable. Solusinya: automated testing. Di React, standar industri = React Testing Library (RTL) + Vitest/Jest. Filosofinya unik: test dari sudut pandang USER, bukan implementasi internal. User klik tombol → cek hasil di layar. User ketik form → cek error message muncul. Bukan: cek state hook value berapa, render method dipanggil berapa kali (anti-pattern).
💡 "Test what users see, not how component works." Quote utama dari Kent C. Dodds, kreator React Testing Library. Test yang fokus ke implementasi pecah pas refactor (walaupun behavior sama). Test yang fokus ke USER survive refactor. Filosofi ini bikin RTL jadi standar — sebelumnya orang pake Enzyme yang test implementasi (dan banyak ditinggalin sekarang).
React Testing Library (RTL) adalah standar untuk testing komponen React. Filosofinya: test dari sudut pandang user, bukan implementasi.
Setup:
import { render, screen, fireEvent } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import Counter from "./Counter"
1. render — render komponen ke virtual DOM:
render(<Counter />)
2. screen — akses elemen yang sudah di-render:
screen.getByText("Halo") // cari berdasarkan teks
screen.getByRole("button") // cari berdasarkan role
screen.getByLabelText("Email") // cari berdasarkan label
screen.getByPlaceholderText("...") // cari berdasarkan placeholder
screen.getByTestId("my-element") // cari berdasarkan data-testid
3. fireEvent vs userEvent:
// fireEvent — simulasi event langsung
fireEvent.click(screen.getByRole("button"))
// userEvent — simulasi interaksi user (lebih realistis)
const user = userEvent.setup()
await user.click(screen.getByRole("button"))
await user.type(screen.getByRole("textbox"), "Halo")
Contoh test lengkap:
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c + 1)}>Tambah</button>
</div>
)
}
test("counter bertambah saat tombol diklik", async () => {
render(<Counter />)
const user = userEvent.setup()
expect(screen.getByText("Count: 0")).toBeInTheDocument()
await user.click(screen.getByRole("button", { name: "Tambah" }))
expect(screen.getByText("Count: 1")).toBeInTheDocument()
})
Pattern testing:
- Render test — apakah komponen render tanpa error?
- Interaction test — apakah klik/input bekerja?
- Async test — gunakan
findByTextuntuk elemen yang muncul async - Snapshot test —
expect(container).toMatchSnapshot()
Async Testing dengan waitFor
Banyak bagian React async: fetching data, animasi, state update bertahap. Pakai waitFor atau findBy*:
import { render, screen, waitFor } from "@testing-library/react";
test("menampilkan user setelah fetch", async () => {
render(<UserProfile userId={1} />);
// findBy* otomatis tunggu sampai elemen muncul (max 1000ms default)
expect(await screen.findByText("Budi")).toBeInTheDocument();
// Atau waitFor untuk kondisi custom
await waitFor(() => {
expect(screen.getByRole("status")).toHaveTextContent("Loaded");
});
});
act() Warning
Kamu mungkin lihat warning "not wrapped in act()". Biasanya artinya: ada state update yang terjadi setelah test selesai. Solusi:
- Pakai
await user.click(...)— userEvent otomatis wrap dengan act. - Untuk update manual, pakai
await act(async () => { ... }).
Mocking Fetch / Module
import { vi } from "vitest";
test("handle API error", async () => {
vi.spyOn(global, "fetch").mockResolvedValue({
ok: false,
status: 500,
} as Response);
render(<App />);
expect(await screen.findByText(/gagal memuat/i)).toBeInTheDocument();
});
Test yang baik meniru cara user berinteraksi. Hindari test yang bergantung pada implementasi internal. Preferensi: userEvent > fireEvent, findBy > waitFor + getBy.*
🎭 Analogi sehari-hari
Testing manual itu kayak QA inspeksi pabrik manual — tiap produk keluar dicek satu-satu, capek, gak konsisten, lupa cek cabang tertentu. Automated test = scanner barcode otomatis — tiap produk lewat conveyor, scanner cek standards, lapor langsung kalo ada yang gagal. Lebih cepat, konsisten, gak miss. RTL filosofi "test from user perspective" = scanner yang fokus ke hasil akhir produk (apakah lampu nyala? apakah kemasan rapi?) bukan proses internal pabrik (apakah baut dipasang oleh tangan A atau B). Pas ada perbaikan proses (refactor), scanner gak perlu di-update — output produk tetep dicek dengan standar yang sama.
⚠️ Jebakan yang sering ditemui
- Test implementasi detail —
expect(useState).toHaveBeenCalled()SALAH. Test behavior visible ke user. - Pakai
getByTestIdeverywhere — last resort. PrefergetByRole,getByLabelText,getByText(lebih a11y-aligned). fireEvent.click()instead ofuserEvent.click()— fireEvent skip beberapa event yang user beneran trigger (focus, mouseover). userEvent lebih realistis.- Forget
awaitdi userEvent —user.click()async — wajibawaitatau gak jalan urut. - Test yang gak deterministik — pakai
Date.now(), random, network — bikin flaky. Mock atau pakaivi.useFakeTimers(). - Test render kosong —
render(<Component />); expect(true).toBe(true)— bukan test, cuma compile check. Cek output user. - Pakai
act()manual di kebanyakan case —userEventudah auto-wrap. Pakaiact()cuma untuk kasus advanced. - Test setiap baris — coverage 100% gak berarti zero bug. Test yang penting: happy path, error path, edge case.
🎯 Test apa, skip apa?
- Critical user flow (login, checkout, submit form) → test ✓ wajib
- Component dengan complex state/conditional → test ✓
- Util function pure → test ✓ (gampang test, high value)
- Custom hook → test ✓ pakai
renderHookdari RTL- Static UI component (Header, Footer tanpa logic) → skip atau snapshot
- Library wrapper sederhana → biasanya skip
- Generated code (auto-gen by tool) → skip
- Third-party component yang udah di-test maintainer → skip
Aturan: 80% effort di 20% feature critical. Skip test yang cuma boost coverage tanpa catch bug real.
Modern Stack 2026
# Setup project
npm install --save-dev vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
# vitest.config.ts
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: "./src/setupTests.ts"
}
})
# setupTests.ts
import "@testing-library/jest-dom"
Vitest > Jest karena: native ESM, lebih cepat, config simpler, integrate baik dengan Vite. Project hypersheets-frontend pakai Vitest.
TL;DR: React Testing Library + Vitest = standar testing React 2026. Filosofi: test dari user perspective, bukan implementasi. Query: getByRole > getByLabelText > getByText > getByTestId (last resort). Interaksi: userEvent > fireEvent (lebih realistis). Async: findBy* > waitFor + getBy. Mock fetch dengan vi.spyOn. Coverage tinggi gak berarti zero bug — fokus test pada critical user flow + complex logic. Skip static UI tanpa logic.