kodingup.camp

Beranda/Next.js Mastery: Full-Stack, Auth & Deploy

Testing

Modul 9 dari 115 menit bacaTesting

Isi modul

    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

    1. Menulis JSX di file .js lalu mengetesnya dengan Vitest. Next.js mau menerima JSX di .js, tapi Vite/Vitest menolak dengan error "JSX syntax is disabled". Beri ekstensi .jsx pada file komponen yang berisi JSX (itulah sebabnya komponen proyek akhir memakai .jsx).
    2. Mencoba unit test async Server Component dengan Vitest. Belum didukung. Pisahkan logika ke function murni untuk unit test, dan uji komponen async lewat Playwright.
    3. Mencari elemen lewat class CSS (container.querySelector('.btn-primary')). Test jadi rapuh saat desain berubah. Pakai getByRole atau getByLabelText.
    4. E2E terhadap next dev. Mode development lebih lambat dan perilakunya berbeda (kompilasi on-demand, overlay error). Jalankan terhadap npm run build + npm run start.
    5. 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

    1. Mudah, Tambahkan unit test untuk slugify yang memastikan string kosong menghasilkan string kosong, dan judul yang hanya berisi simbol ('!!!') juga menghasilkan string kosong.
    2. Sedang, Tulis component test untuk komponen FieldError dari PostForm: tidak merender apa pun bila errors kosong, dan menampilkan pesan pertama bila ada. (Export dulu FieldError dari file-nya.)
    3. 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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    9. Testing | Tutorial Nextjs untuk Pemula IndonesiaRobertus Adrian · Bahasa IndonesiaBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.