Apa itu Component Testing?
Component testing adalah pengujian komponen UI (React, Vue, dsb.) secara terisolasi — tanpa menjalankan seluruh aplikasi. Kita merender komponen ke dalam DOM virtual, lalu berinteraksi dan mengecek hasilnya.
Mengapa Component Testing Penting?
- Lebih cepat daripada E2E karena tidak butuh browser sungguhan
- Menguji behavior — apa yang dilihat dan dilakukan user, bukan detail implementasi
- Mudah di-debug — scope pengujian kecil dan terfokus
Setup: Vitest + Testing Library
Kombinasi Vitest sebagai test runner dan @testing-library/react untuk merender & query komponen adalah standar industri saat ini.
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Konfigurasi vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
},
});
Render & Query
Gunakan render() untuk merender komponen, lalu screen untuk mencari elemen:
import { render, screen } from "@testing-library/react";
import Greeting from "./Greeting";
it("menampilkan pesan salam", () => {
render(<Greeting name="Budi" />);
expect(screen.getByText("Halo, Budi!")).toBeInTheDocument();
});
Query yang Sering Digunakan
getByRole("button", { name: "Kirim" })— cari berdasarkan role aksesibilitasgetByText("Selamat datang")— cari berdasarkan teks yang terlihatgetByLabelText("Email")— cari input berdasarkan labelgetByPlaceholderText("Cari...")— cari berdasarkan placeholdergetByTestId("submit-btn")— fallback menggunakan data-testid
Query Hierarchy — Pilih Query yang Tepat
Testing Library punya aturan prioritas yang mencerminkan bagaimana user menemukan elemen. Semakin tinggi, semakin aksesibel dan realistis:
getByRole— paling direkomendasikan. Mencerminkan bagaimana assistive tech (screen reader) menavigasi halaman. Selalu coba ini dulu.getByLabelText— untuk form. Mencerminkan bagaimana user mengisi input berdasarkan label.getByPlaceholderText— hanya jika tidak ada label (hindari di produksi).getByText— untuk konten non-interaktif: paragraf, heading, tombol tanpa role eksplisit.getByDisplayValue— untuk form yang sudah terisi.getByAltText— khusus<img>dan elemen denganalt.getByTitle— fallback untuktitleattribute.getByTestId— last resort. Pakai hanya kalau elemen tidak bisa dijangkau cara lain.
Tiga Varian: getBy, queryBy, findBy
Setiap query punya tiga varian yang behaviour-nya sangat berbeda — salah pilih, test-mu bisa menyesatkan:
| Varian | Tidak ketemu | Async? | Kapan pakai |
|---|---|---|---|
getBy* | Lempar error | Tidak | Elemen harus ada sekarang |
queryBy* | Return null | Tidak | Assert elemen tidak ada (expect(...).toBeNull()) |
findBy* | Retry sampai timeout lalu error | Ya (await) | Elemen muncul setelah async (fetch, delay) |
// getBy — elemen harus ada SEKARANG
expect(screen.getByRole("heading")).toBeInTheDocument();
// queryBy — assert elemen TIDAK ada
expect(screen.queryByText("Error")).toBeNull();
// findBy — tunggu elemen muncul
const data = await screen.findByText("Data loaded");
userEvent vs fireEvent
Dua cara mensimulasikan interaksi — beda tingkat realismenya:
fireEvent— mendispatch satu event DOM langsung (click,change). Cepat tapi tidak realistis: tidak memicu focus chain, hover, keydown, dll.userEvent— mensimulasikan rangkaian event yang sebenarnya terjadi saat user mengetik/klik. Contoh:user.type()memicufocus → keydown → input → keyupper karakter.user.click()memicupointerdown → mousedown → focus → pointerup → mouseup → click.
// fireEvent — satu event, sering miss bug
fireEvent.change(input, { target: { value: "halo" } });
// userEvent — rangkaian event realistis (DIREKOMENDASIKAN)
const user = userEvent.setup();
await user.type(input, "halo"); // mengetik huruf per huruf
Pakai userEvent sebagai default. fireEvent hanya jika butuh trigger event spesifik yang userEvent tidak sediakan (misal: scroll).
Interaksi User dengan userEvent
userEvent mensimulasikan interaksi user yang lebih realistis dibanding fireEvent:
import userEvent from "@testing-library/user-event";
it("mengirim form saat tombol diklik", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.type(screen.getByLabelText("Password"), "rahasia123");
await user.click(screen.getByRole("button", { name: "Masuk" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "[email protected]",
password: "rahasia123",
});
});
Testing Async: waitFor
Untuk komponen yang melakukan fetch data atau update async, gunakan waitFor atau findBy*:
it("menampilkan data setelah loading", async () => {
render(<UserProfile userId={1} />);
// Awalnya tampil loading
expect(screen.getByText("Memuat...")).toBeInTheDocument();
// Tunggu data muncul
const name = await screen.findByText("Budi Santoso");
expect(name).toBeInTheDocument();
// Loading sudah hilang
expect(screen.queryByText("Memuat...")).not.toBeInTheDocument();
});
Best Practices
- Test behavior, bukan implementasi — jangan test state internal atau method, test apa yang user lihat
- Gunakan query aksesibilitas —
getByRolelebih baik daripadagetByTestId - Gunakan userEvent daripada fireEvent untuk interaksi yang realistis
- Hindari snapshot berlebihan — snapshot menangkap detail yang tidak relevan