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-141). 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-72). Baris terakhir memakai createJestConfig(customJestConfig) dengan alasan yang ditulis di komentar: konfigurasi Next.js dibaca secara async (jest.config.js:29-303).
Objek customJestConfig hanya memuat empat kunci. Sisanya diisi oleh next/jest, termasuk transformasi TypeScript dan pemuatan .env.
| Kunci | Nilai | Akibat pada test |
|---|---|---|
setupFilesAfterEnv | <rootDir>/jest.setup.js | Berkas setup dijalankan sebelum setiap test (jest.config.js:10-124) |
moduleDirectories | node_modules, <rootDir>/ | Import absolut dihitung dari akar repository (jest.config.js:155) |
testEnvironment | jest-environment-jsdom | render dari @testing-library/react mendapat DOM (jest.config.js:176) |
moduleNameMapper | tiga pola regex | Alias @/, ~/, dan modul SVG diarahkan ke berkas nyata (jest.config.js:22-267) |
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.Tiga entri moduleNameMapper bekerja bersamaan (jest.config.js:22-267). 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-348). 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.tsxtanpa mengganti entrimoduleNameMappermembuat~/svg/Logo.svgdiperlakukan 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-59). Keduanya didaftarkan lewat setupFilesAfterEnv, jadi berlaku untuk semua berkas test tanpa perlu import ulang (jest.config.js:10-124).
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.| Berkas test | Yang diuji | Assertion |
|---|---|---|
| src/__tests__/pages/HomePage.test.tsx:1-1510 | Render @/app/page, lalu cari teks paragraf | expect(heading).toBeInTheDocument() |
| src/lib/__tests__/og.test.ts:1-1911 | String URL dari openGraph dengan dan tanpa templateTitle | not.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-1510). Teks yang dicari itu berasal dari paragraf di src/app/page.tsx (src/app/page.tsx:30-348). 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-1911). 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-2612). 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-1510). Begitu teks paragraf disrc/app/page.tsxdiganti saat menyesuaikan starter, assertiongetByTextikut 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-141). 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.