Component Testing — Testing

Apa itu Component Testing? Component testing adalah pengujian komponen UI (React, Vue, dsb.) secara terisolasi — tanpa menjalankan seluruh aplikasi. Kita…

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 dengan getByText("Kirim") tetap lulus. Test yang mencari dengan getByRole("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();
  });
});

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:

  1. getByRole: lewat peran aksesibilitas (button, textbox, link, heading, checkbox) dan namanya. Coba ini dulu.
  2. getByLabelText: untuk kolom form yang punya label.
  3. getByPlaceholderText: kalau gak ada label (sebaiknya ada).
  4. getByText: untuk teks biasa, paragraf, pesan.
  5. getByDisplayValue, getByAltText, getByTitle: untuk kasus khusus.
  6. getByTestId: jalan terakhir, lewat atribut data-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:

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

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.

Yang akan kamu pelajari