Kenapa ini penting
Setiap kali kamu menambah fitur, ada risiko merusak fitur lama. Mengubah validasi judul bisa saja membuat form edit gagal; mengubah matcher Proxy bisa saja membuka halaman admin untuk umum. Kalau satu-satunya cara memeriksa adalah membuka browser dan mengklik semuanya satu per satu, lama-lama kamu akan melewatkan sesuatu, apalagi saat aplikasi sudah punya puluhan halaman.
Test otomatis adalah kode yang memeriksa kodemu. Ditulis sekali, dijalankan ribuan kali dalam hitungan detik, dan tidak pernah lupa langkah. Test juga menjadi dokumentasi hidup: membaca test('admin tanpa login dilempar ke /login') langsung memberi tahu perilaku yang diharapkan.
Analogi: test itu seperti daftar periksa pilot sebelum terbang. Pilot berpengalaman pun tetap memakainya, bukan karena tidak kompeten, tapi karena satu langkah terlewat bisa berakibat besar, dan daftar periksa membuat pengecekan konsisten setiap kali.
Konsep inti
Piramida testing
| Jenis | Menguji | Kecepatan | Alat di modul ini |
|---|---|---|---|
| Unit test | Satu function atau komponen secara terpisah | Sangat cepat (milidetik) | Vitest |
| Component test | Komponen React dirender di DOM tiruan | Cepat | Vitest + Testing Library + jsdom |
| End-to-end (E2E) | Seluruh aplikasi lewat browser sungguhan | Lambat (detik) | Playwright |
Prinsip umumnya: banyak unit test (murah dan cepat), sejumlah component test, dan sedikit E2E untuk alur paling penting (login, membuat artikel, checkout).
Apa yang layak dites?
Kamu tidak perlu mengetes semuanya. Prioritaskan tiga hal: logika yang punya aturan (validasi, perhitungan, pembuatan slug), alur yang mahal kalau rusak (login, menyimpan data, pembayaran), dan bug yang pernah terjadi (tulis test yang mereproduksi bug sebelum memperbaikinya, supaya bug itu tidak kembali diam-diam). Hal yang murni tampilan, seperti warna tombol atau jarak antar elemen, biasanya lebih efisien diperiksa dengan mata.
Vitest + React Testing Library
Vitest (versi 5.0.5) adalah test runner yang cepat dan API-nya mirip Jest (test, describe, expect). React Testing Library (@testing-library/react 16.3.3) merender komponen dan menyediakan cara mencari elemen seperti pengguna mencarinya: berdasarkan role, label, atau teks, bukan berdasarkan class CSS. Filosofinya: semakin mirip test dengan cara pengguna memakai aplikasi, semakin besar kepercayaan yang diberikan test itu. jsdom (30.1.3) menyediakan DOM tiruan di Node.js, dan @vitejs/plugin-react (6.1.2) membuat Vitest mengerti JSX.
Batasan penting dari dokumentasi Next.js: Vitest belum mendukung async Server Component. Komponen sinkron (Server atau Client) dan function biasa bisa di-unit test; komponen async yang mengambil data sebaiknya diuji lewat E2E. Karena itu, kebiasaan bagus adalah memisahkan logika ke function murni (misalnya slugify) yang gampang dites.
Query Testing Library yang paling sering dipakai:
| Query | Contoh |
|---|---|
getByRole |
screen.getByRole('button', { name: 'Simpan' }) (paling dianjurkan) |
getByLabelText |
screen.getByLabelText('Judul') |
getByText |
screen.getByText('Belum ada artikel.') |
queryBy... |
Mengembalikan null bila tidak ada (untuk mengecek elemen tidak tampil) |
findBy... |
Versi async, menunggu elemen muncul |
Playwright untuk E2E
Playwright (@playwright/test 1.64.1) mengendalikan browser sungguhan (Chromium, Firefox, WebKit) untuk menjalankan skenario seperti pengguna: buka halaman, isi form, klik, periksa hasil. Untuk Next.js, jalankan E2E terhadap build produksi (npm run build lalu npm run start) agar perilakunya sama dengan yang akan diterima pengguna. Opsi webServer di playwright.config bisa menyalakan server otomatis sebelum test.
Locator Playwright juga berbasis role dan label (page.getByRole, page.getByLabel, page.getByPlaceholder), dan expect(...) di Playwright menunggu otomatis sampai kondisi terpenuhi (default 5 detik), sehingga kamu jarang butuh sleep manual.
Contoh kode
Pasang alat test (perintah dari dokumentasi Next.js untuk proyek JavaScript):
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom
npm install -D @playwright/test
npx playwright install chromium # unduh browser untuk E2E (sekali saja)
Konfigurasi Vitest, termasuk alias @/ supaya import sama seperti di Next.js:
// vitest.config.mjs
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { fileURLToPath } from 'node:url'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': fileURLToPath(new URL('./', import.meta.url)) },
},
test: {
environment: 'jsdom',
include: ['__tests__/**/*.test.{js,jsx}'],
},
})
Tambahkan script di package.json:
{
"scripts": {
"test": "vitest run",
"test:e2e": "playwright test"
}
}
Unit test untuk function murni:
// __tests__/slugify.test.js
import { describe, expect, test } from 'vitest'
import { slugify } from '@/lib/slugify'
describe('slugify', () => {
test('mengubah judul jadi slug', () => {
expect(slugify('Belajar Next.js 16!')).toBe('belajar-next-js-16')
})
test('membuang aksen dan spasi berlebih', () => {
expect(slugify(' Café Enak ')).toBe('cafe-enak')
})
})
Component test untuk SubmitButton:
// __tests__/SubmitButton.test.jsx
import { afterEach, expect, test } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import { SubmitButton } from '@/components/SubmitButton'
afterEach(cleanup) // bersihkan DOM tiruan antar test
test('menampilkan teks tombol dan aktif saat tidak pending', () => {
render(
<form>
<SubmitButton>Simpan</SubmitButton>
</form>
)
const button = screen.getByRole('button', { name: 'Simpan' })
expect(button.disabled).toBe(false)
expect(button.getAttribute('type')).toBe('submit')
})
npm test
# → ✓ __tests__/slugify.test.js (2 tests)
# → ✓ __tests__/SubmitButton.test.jsx (1 test)
# → Test Files 2 passed (2)
# → Tests 3 passed (3)
Konfigurasi Playwright yang menyalakan server produksi otomatis:
// playwright.config.mjs
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:3000' },
webServer: {
command: 'npm run start', // jalankan `npm run build` dulu sebelum test
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
Test E2E untuk alur penting proyek akhir:
// e2e/blog.spec.mjs
import { test, expect } from '@playwright/test'
test('beranda menampilkan artikel dan bisa dibuka', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Artikel Terbaru' })).toBeVisible()
await page.getByRole('link', { name: 'Halo Dunia' }).click()
await expect(page).toHaveURL(/\/blog\/halo-dunia$/)
await expect(page.getByRole('heading', { level: 1, name: 'Halo Dunia' })).toBeVisible()
})
test('admin tanpa login dilempar ke /login', async ({ page }) => {
await page.goto('/admin')
await expect(page).toHaveURL(/\/login$/)
})
test('login, buat artikel, lalu tampil di beranda', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Password admin').fill('rahasia-belajar-123')
await page.getByRole('button', { name: 'Masuk' }).click()
await expect(page).toHaveURL(/\/admin$/)
const judul = `Uji E2E ${Date.now()}` // judul unik agar slug tidak bentrok
await page.goto('/admin/new')
await page.getByLabel('Judul').fill(judul)
await page.getByLabel('Ringkasan').fill('Ringkasan dari tes end-to-end.')
await page.getByLabel(/^Isi/).fill('Paragraf isi yang cukup panjang untuk lolos validasi.')
await page.getByLabel('Terbitkan').check()
await page.getByRole('button', { name: 'Simpan' }).click()
await expect(page).toHaveURL(/\/admin$/)
await page.goto('/')
await expect(page.getByRole('link', { name: judul })).toBeVisible()
})
npm run build && npm run test:e2e
# → Running 3 tests using 1 worker
# → 3 passed (5.1s)
Test ketiga membuktikan banyak hal sekaligus: Proxy, login, cookie session, Server Action, validasi Zod, penulisan database, updateTag, dan caching beranda. Itulah kekuatan E2E, sekaligus alasan jumlahnya sebaiknya sedikit: satu test lambat bisa mencakup banyak lapisan.
Kesalahan umum
- Menulis JSX di file
.jslalu mengetesnya dengan Vitest. Next.js mau menerima JSX di.js, tapi Vite/Vitest menolak dengan error "JSX syntax is disabled". Beri ekstensi.jsxpada file komponen yang berisi JSX (itulah sebabnya komponen proyek akhir memakai.jsx). - Mencoba unit test async Server Component dengan Vitest. Belum didukung. Pisahkan logika ke function murni untuk unit test, dan uji komponen async lewat Playwright.
- Mencari elemen lewat class CSS (
container.querySelector('.btn-primary')). Test jadi rapuh saat desain berubah. PakaigetByRoleataugetByLabelText. - E2E terhadap
next dev. Mode development lebih lambat dan perilakunya berbeda (kompilasi on-demand, overlay error). Jalankan terhadapnpm run build+npm run start. - Test yang saling bergantung atau memakai data tetap. Membuat artikel berjudul "Tes" di setiap run akan gagal di run kedua karena slug bentrok. Pakai data unik (misalnya
Date.now()) atau reset database test.
Latihan
- Mudah, Tambahkan unit test untuk
slugifyyang memastikan string kosong menghasilkan string kosong, dan judul yang hanya berisi simbol ('!!!') juga menghasilkan string kosong. - Sedang, Tulis component test untuk komponen
FieldErrordariPostForm: tidak merender apa pun bilaerrorskosong, dan menampilkan pesan pertama bila ada. (Export duluFieldErrordari file-nya.) - Menantang, Tulis test E2E yang memastikan validasi berjalan: login, buka
/admin/new, isi judul "Hi" saja, klik Simpan, lalu pastikan pesan "Judul minimal 3 karakter" tampil dan URL tetap/admin/new.
Lihat solusi
// 1. tambahkan ke __tests__/slugify.test.js
test('input kosong atau simbol saja menghasilkan string kosong', () => {
expect(slugify('')).toBe('')
expect(slugify('!!!')).toBe('')
})
// 2. __tests__/FieldError.test.jsx (di components/PostForm.jsx: export function FieldError ...)
import { afterEach, expect, test } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import { FieldError } from '@/components/PostForm'
afterEach(cleanup)
test('tidak merender apa pun bila tidak ada error', () => {
const { container } = render(<FieldError errors={[]} />)
expect(container.innerHTML).toBe('')
})
test('menampilkan pesan error pertama', () => {
render(<FieldError errors={['Judul minimal 3 karakter', 'lainnya']} />)
expect(screen.getByText('Judul minimal 3 karakter')).toBeTruthy()
expect(screen.queryByText('lainnya')).toBeNull()
})
// 3. e2e/validasi.spec.mjs
import { test, expect } from '@playwright/test'
test('judul terlalu pendek ditolak', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Password admin').fill('rahasia-belajar-123')
await page.getByRole('button', { name: 'Masuk' }).click()
await expect(page).toHaveURL(/\/admin$/)
await page.goto('/admin/new')
await page.getByLabel('Judul').fill('Hi')
await page.getByRole('button', { name: 'Simpan' }).click()
await expect(page.getByText('Judul minimal 3 karakter')).toBeVisible()
await expect(page).toHaveURL(/\/admin\/new$/)
})
Ringkasan
- Unit test untuk function murni, component test untuk UI sinkron, E2E untuk alur penting; jumlahnya mengecil ke atas piramida.
- Vitest 5 + Testing Library + jsdom +
@vitejs/plugin-reactadalah setup yang dianjurkan dokumentasi Next.js; file berisi JSX harus.jsx. - Vitest belum mendukung async Server Component, uji lewat E2E.
- Cari elemen seperti pengguna:
getByRole,getByLabelText,getByText. - Playwright menjalankan browser sungguhan; uji terhadap build produksi dan pakai data unik per run.