Lompat ke konten
dokumentasi.id
Commit 9a47d33
v2
Star3,4 rb

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

10

Pengujian dengan Jest

Repository ini menjalankan test dengan Jest, dan next/jest dipakai supaya konfigurasi Next.js ikut termuat di lingkungan test. Dua skrip di package.json menjadi pintu masuknya: test menjalankan jest sekali jalan, test:watch menambahkan --watch (package.json:13-14). Skrip test yang sama juga dipanggil pipeline CI, yang dirinci di Otomasi Repository.

next/jest sebagai basis konfigurasi

jest.config.js tidak menyusun konfigurasi Jest dari nol. Berkas itu memanggil nextJest dengan dir: './', menyerahkan objek customJestConfig, lalu mengekspor hasil bungkusannya lewat module.exports (jest.config.js:1-7). Baris terakhir memakai createJestConfig(customJestConfig) dengan alasan yang ditulis di komentar: konfigurasi Next.js dibaca secara async (jest.config.js:29-30).

Objek customJestConfig hanya memuat empat kunci. Sisanya diisi oleh next/jest, termasuk transformasi TypeScript dan pemuatan .env.

KunciNilaiAkibat pada test
setupFilesAfterEnv<rootDir>/jest.setup.jsBerkas setup dijalankan sebelum setiap test (jest.config.js:10-12)
moduleDirectoriesnode_modules, <rootDir>/Import absolut dihitung dari akar repository (jest.config.js:15)
testEnvironmentjest-environment-jsdomrender dari @testing-library/react mendapat DOM (jest.config.js:17)
moduleNameMappertiga pola regexAlias @/, ~/, dan modul SVG diarahkan ke berkas nyata (jest.config.js:22-26)

Peta konfigurasi ke berkas test

Diagram berikut menghubungkan keempat kunci itu dengan dua suite yang ada, dan menunjukkan bagian mana yang menyelesaikan alias pada masing-masing suite.

graph TD
  nextJest["next/jest: dir './'"] --> config["customJestConfig"]
  config --> env["testEnvironment: jest-environment-jsdom"]
  config --> setup["setupFilesAfterEnv: jest.setup.js"]
  config --> dirs["moduleDirectories: node_modules, rootDir"]
  config --> mapper["moduleNameMapper"]
  mapper --> atAlias["^@/(.*)$ → rootDir/src/$1"]
  mapper --> tildeAlias["^~/(.*)$ → rootDir/public/$1"]
  mapper --> svgAlias["svg → src/__mocks__/svg.tsx"]
  setup --> jestDom["@testing-library/jest-dom/extend-expect"]
  setup --> routerMock["next-router-mock"]
  jestDom --> homeTest["HomePage.test.tsx"]
  routerMock --> homeTest
  atAlias --> homeTest
  tildeAlias --> homeTest
  svgAlias --> svgMock["SvgrMock: ReactComponent + default"]
  svgMock --> homeTest
  atAlias --> ogTest["og.test.ts"]
  homeTest --> domAssert["expect(heading).toBeInTheDocument()"]
  ogTest --> stringAssert["expect(result).toContain(...)"]
  style nextJest fill:#f59e0b,color:#000
  style config fill:#f59e0b,color:#000
  style env fill:#f59e0b,color:#000
  style setup fill:#f59e0b,color:#000
  style dirs fill:#f59e0b,color:#000
  style mapper fill:#f59e0b,color:#000
  style atAlias fill:#f59e0b,color:#000
  style tildeAlias fill:#f59e0b,color:#000
  style svgAlias fill:#f59e0b,color:#000
  style svgMock fill:#8b5cf6,color:#fff
  style routerMock fill:#8b5cf6,color:#fff
  style jestDom fill:#8b5cf6,color:#fff
  style domAssert fill:#2d7d46,color:#fff
  style stringAssert fill:#2d7d46,color:#fff
%% fig: Jalur konfigurasi Jest ke dua berkas test
%% desc: Keempat kunci customJestConfig mengarahkan alias dan mock sampai kedua suite berhasil menyelesaikan import-nya.
Fig. 1: Jalur konfigurasi Jest ke dua berkas testKeempat kunci customJestConfig mengarahkan alias dan mock sampai kedua suite berhasil menyelesaikan import-nya.

Tiga entri moduleNameMapper bekerja bersamaan (jest.config.js:22-26). Pola ^@/(.*)$ memetakan ke <rootDir>/src/$1, sehingga import HomePage from '@/app/page' di suite halaman dan import { openGraph } from '@/lib/og' di suite utilitas menunjuk ke sumber TypeScript yang sama dengan yang dipakai aplikasi. Pola ^~/(.*)$ memetakan ke <rootDir>/public/$1, arah yang dipakai src/app/page.tsx untuk import Logo from '~/svg/Logo.svg' (src/app/page.tsx:30-34). Entri ketiga, '^.+\\.(svg)$', menghentikan Jest sebelum berkas SVG asli dibaca dan menggantinya dengan mock.

Mock SVG di src/__mocks__/svg.tsx

Mock itu bukan komponen SVG hasil kompilasi SVGR. Berkasnya membuat satu komponen dengan React.forwardRef, merender <svg ref={ref} {...props} />, lalu mengekspornya dua kali: sebagai ReactComponent dan sebagai default (src/mocks/svg.tsx:1-8). Karena kedua bentuk ekspor identik, suite tetap lolos baik ketika komponen diimpor sebagai default maupun sebagai named import.

Konsekuensinya, atribut seperti className='w-16' hanya diteruskan sebagai props biasa ke elemen <svg>. Test tidak membuktikan apa pun tentang hasil kompilasi SVGR yang sebenarnya, hanya tentang kontrak impor modulnya.

⚠️ Menghapus src/__mocks__/svg.tsx tanpa mengganti entri moduleNameMapper membuat ~/svg/Logo.svg diperlakukan sebagai modul JavaScript biasa, dan suite halaman gagal saat import, bukan saat assertion.

Berkas setup

jest.setup.js hanya berisi dua baris aktif. Baris pertama mengimpor @testing-library/jest-dom/extend-expect, yang menyediakan matcher tambahan seperti toBeInTheDocument. Baris kedua memanggil jest.mock('next/router', () => require('next-router-mock')), sehingga modul next/router diganti implementasi mock di seluruh suite (jest.setup.js:1-5). Keduanya didaftarkan lewat setupFilesAfterEnv, jadi berlaku untuk semua berkas test tanpa perlu import ulang (jest.config.js:10-12).

Dua suite dan gaya assertion-nya

Kedua suite menguji hal yang berbeda bentuknya: satu memeriksa DOM hasil render, satu memeriksa string hasil pemanggilan fungsi murni.

graph LR
  cmd["pnpm run test"] --> homeSuite["describe: Homepage"]
  cmd --> ogSuite["describe: Open Graph function should work correctly"]
  homeSuite --> renderCall["render(<HomePage />)"]
  renderCall --> query["screen.getByText(/A starter for Next.js/i)"]
  query --> domAssert["expect(heading).toBeInTheDocument()"]
  ogSuite --> noTemplate["openGraph tanpa templateTitle"]
  noTemplate --> notContain["not.toContain(&templateTitle=)"]
  ogSuite --> withTemplate["openGraph dengan templateTitle"]
  withTemplate --> contain["toContain(&templateTitle=Test%20Template%20Title)"]
  style cmd fill:#f59e0b,color:#000
  style homeSuite fill:#336791,color:#fff
  style ogSuite fill:#336791,color:#fff
  style renderCall fill:#8b5cf6,color:#fff
  style noTemplate fill:#8b5cf6,color:#fff
  style withTemplate fill:#8b5cf6,color:#fff
  style domAssert fill:#2d7d46,color:#fff
  style notContain fill:#2d7d46,color:#fff
  style contain fill:#2d7d46,color:#fff
%% fig: Dua gaya assertion pada dua suite
%% desc: Suite Homepage memeriksa hasil render di DOM, suite Open Graph memeriksa string URL dengan dua cabang templateTitle.
Fig. 2: Dua gaya assertion pada dua suiteSuite Homepage memeriksa hasil render di DOM, suite Open Graph memeriksa string URL dengan dua cabang templateTitle.
Berkas testYang diujiAssertion
src/__tests__/pages/HomePage.test.tsx:1-15Render @/app/page, lalu cari teks paragrafexpect(heading).toBeInTheDocument()
src/lib/__tests__/og.test.ts:1-19String URL dari openGraph dengan dan tanpa templateTitlenot.toContain('&templateTitle=') dan toContain('&templateTitle=Test%20Template%20Title')

Suite halaman merender <HomePage /> dan mencari satu elemen dengan screen.getByText(/A starter for Next.js/i) (src/__tests__/pages/HomePage.test.tsx:1-15). Teks yang dicari itu berasal dari paragraf di src/app/page.tsx (src/app/page.tsx:30-34). Karena getByText gagal ketika tidak ada elemen yang cocok, test ini sekaligus membuktikan bahwa alias, mock SVG, dan setup jsdom berjalan.

Suite utilitas memanggil openGraph dua kali. Panggilan tanpa templateTitle harus menghasilkan string tanpa &templateTitle=, sedangkan panggilan dengan templateTitle harus menghasilkan &templateTitle=Test%20Template%20Title (src/lib/__tests__/og.test.ts:1-19). Kedua ekspektasi itu mengunci percabangan di src/lib/og.ts, tempat templateTitle di-encodeURIComponent lalu ditempel sebagai query hanya jika nilainya ada (src/lib/og.ts:19-26). Perhatikan bahwa %20 muncul karena encodeURIComponent mengubah spasi, jadi assertion menuliskan bentuk ter-encode, bukan judul aslinya.

⚠️ Berkas test halaman dibuka dengan komentar // !STARTERCONF You should delete this page (src/__tests__/pages/HomePage.test.tsx:1-15). Begitu teks paragraf di src/app/page.tsx diganti saat menyesuaikan starter, assertion getByText ikut gagal. Ganti atau hapus test itu bersamaan dengan mengubah halamannya, bukan sesudahnya.

Menjalankan dan menambah test

pnpm run test menjalankan seluruh suite sekali, pnpm run test:watch mengulang di mode pantau (package.json:13-14). Untuk test baru, tempatkan berkas di direktori __tests__ dengan sufiks .test.ts atau .test.tsx. Selama berkas ada di dalam src/ atau public/, alias @/ dan ~/ bisa langsung dipakai tanpa import relatif bertingkat. Berkas yang memuat JSX harus diberi sufiks .tsx agar transformasi next/jest memperlakukannya sebagai komponen.