Testing React — React

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…

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"), atau getByRole("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()
})

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:

  1. getByRole("button", { name: "Tambah" }): berdasarkan role, yaitu peran elemen (button, heading, textbox, checkbox), dan namanya.
  2. getByLabelText("Email"): input berdasarkan label-nya.
  3. getByPlaceholderText("Cari..."): input berdasarkan placeholder.
  4. getByText("Count: 0"): elemen biasa berdasarkan teksnya.
  5. getByDisplayValue, getByAltText (gambar), getByTitle.
  6. getByTestId("tambah"): berdasarkan atribut data-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:

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

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

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.

Yang akan kamu pelajari