Skip to main content
BlogWeb Dev

Cara Menjalankan Node Test Runner di Next.js untuk Pemula

Cara menjalankan node test runner di Next.js untuk pemula: tulis tes pertama, baca kegagalan, uji handler API, lalu pasang npm test sebagai gerbang di CI.

Matthews Wong

Software Engineer

Oktober 20269 menit baca
Cara Menjalankan Node Test Runner di Next.js untuk Pemula

Ringkasan Utama

Cara menjalankan node test runner di Next.js: tulis file bernama *.test.js yang mengimpor describe dan it dari node:test serta assert dari node:assert/strict, jalankan dengan node --test, lalu pasang npm test sebagai langkah di GitHub Actions sebelum build. Exit code 1 saat ada tes gagal akan menghentikan pipeline, tanpa Jest.

Kamu mengubah satu fungsi kecil, build hijau, lalu seminggu kemudian baru sadar fungsi itu mengembalikan nilai yang salah untuk satu kasus tepi. Tidak ada yang menangkapnya karena tidak ada tes. Biasanya langkah berikutnya adalah mencari framework tes, dan di situ pemula berhenti: Jest, Vitest, konfigurasi, transformer. Padahal Node.js sudah membawa test runner sendiri.

Artikel ini membimbingmu dari folder kosong sampai tes pertama berjalan, gagal dengan sengaja, menguji handler API, lalu menjadi gerbang di GitHub Actions. Semua perintah sudah dijalankan sungguhan dan outputnya disalin apa adanya. Di repo situs ini tes memang hidup di folder tests/ dan dijalankan dengan satu npm script, tanpa framework tes.

Ingin tahu kapan sebuah aturan sebaiknya jadi tes, kapan jadi build gate? Ikuti pelajaran gratis Tes Situs Next.js dengan node:test dan Build Gate. Artikel ini adalah versi praktiknya, langkah demi langkah.

Yang kamu butuhkan

Tiga hal saja, dan tidak ada yang perlu dipasang lewat npm:

  • Node.js 20 atau lebih baru. Dokumentasi Node menandai test runner sebagai stable sejak v20.0.0.
  • Sebuah folder project dengan package.json. Untuk project Next.js, folder yang sudah ada sudah cukup.
  • Terminal, dan kalau mau sampai langkah terakhir, repo di GitHub.

Langkah 1: Siapkan folder dan script npm test

Buat folder kecil untuk berlatih (atau pakai project Next.js-mu, bedanya hanya letak file). Baris type module membuat file .js memakai import, dan script test cukup memanggil node --test tanpa argumen, sehingga Node mencari sendiri file tes berdasarkan namanya.

{
  "name": "demo",
  "private": true,
  "type": "module",
  "scripts": { "test": "node --test" }
}

# Check your Node version (20 or newer)
node --version

Aturan namanya penting dan akan muncul lagi di tabel error. Tanpa argumen, Node menjalankan file yang cocok dengan pola seperti **/*.test.js, **/*-test.js, **/test-*.js, atau file apa pun di folder bernama test. Folder bernama tests tidak ikut dengan sendirinya, jadi namai filemu *.test.js.

Langkah 2: Tulis fungsi murni dan tes pertamamu

Mulai dari fungsi murni: input masuk, output keluar, tanpa jaringan dan tanpa database. Itu jenis kode yang paling mudah dan paling murah diuji. Fungsi slugify di bawah mengubah judul menjadi bagian URL.

// lib/slugify.js
export function slugify(text) {
  return text
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

// tests/slugify.test.js
import assert from "node:assert/strict";
import { describe, it } from "node:test";

// .js is required: relative imports in ES modules need the extension
import { slugify } from "../lib/slugify.js";

const cases = [
  ["Hello World", "hello-world"],
  ["  Belajar   Next.js  ", "belajar-next-js"],
  ["", ""],
];

describe("slugify", () => {
  for (const [input, expected] of cases) {
    it(`${JSON.stringify(input)} -> ${JSON.stringify(expected)}`, () => {
      assert.equal(slugify(input), expected);
    });
  }
});

Perhatikan tabel cases. Satu loop mengubah setiap baris menjadi tes bernama sendiri, jadi menambah kasus tepi cukup satu baris data, dan kegagalan menyebut persis input yang rusak. Pola yang sama dipakai tes Accept header di repo ini. Impor assert dari node:assert/strict supaya assert.equal membandingkan ketat seperti strictEqual, bukan dengan ==.

Langkah 3: Jalankan, lalu rusak dengan sengaja

Jalankan npm test dan baca outputnya. Setelah itu ubah satu nilai yang diharapkan menjadi salah (misalnya hello_world), jalankan lagi, dan perhatikan tiga hal: tanda silang di tes yang gagal, bagian actual dan expected, dan exit code 1. Exit code inilah yang nanti dibaca GitHub Actions.

npm test

# Output when everything passes (durations differ on your machine):
# ▶ slugify
#   ✔ "Hello World" -> "hello-world"
#   ✔ "  Belajar   Next.js  " -> "belajar-next-js"
#   ✔ "" -> ""
# ✔ slugify
# ℹ tests 3
# ℹ pass 3
# ℹ fail 0

# Now change "hello-world" to "hello_world" in the cases table and run again:
#   ✖ "Hello World" -> "hello_world"
#   AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
#   + actual - expected
#   + 'hello-world'
#   - 'hello_world'
# ℹ fail 1

echo $?
# 1

Membaca kegagalan sama pentingnya dengan menulis tes. Tanda plus adalah nilai asli dari fungsimu, tanda minus adalah yang kamu harapkan. Kalau keduanya terbalik di kepalamu, kamu akan memperbaiki sisi yang salah.

Langkah 4: Uji handler API yang mengembalikan Response

Sebuah route handler tidak harus dites lewat server sungguhan. Pisahkan logikanya menjadi fungsi yang mengembalikan Response, lalu panggil fungsi itu langsung. Response.json membuat objek Response dengan body JSON, dan kamu bisa membaca status, header, dan body-nya. Repo ini melakukan hal serupa: tes memanggil helper methodNotAllowed dan memeriksa status 405, header Allow, dan kode di body JSON.

// lib/api-error.js
export function methodNotAllowed(allowed) {
  return Response.json(
    { success: false, code: "method_not_allowed" },
    { status: 405, headers: { Allow: allowed.join(", ") } },
  );
}

// tests/api-error.test.js
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { methodNotAllowed } from "../lib/api-error.js";

describe("methodNotAllowed", () => {
  it("answers 405 with an Allow header and a JSON body", async () => {
    const response = methodNotAllowed(["POST"]);
    assert.equal(response.status, 405);
    assert.equal(response.headers.get("allow"), "POST");
    const body = await response.json();
    assert.equal(body.code, "method_not_allowed");
  });
});

// Wrong: no await, so body is a promise and body.code is undefined.
// The failure says "actual: undefined", which hides the real cause.
const body = methodNotAllowed(["POST"]).json();

// Right: the test function is async and the promise is awaited.
const body = await methodNotAllowed(["POST"]).json();

Karena response.json mengembalikan promise, tesnya harus async dan memakai await. Lupa await adalah kesalahan paling umum di langkah ini, dan pesannya membingungkan: actual bernilai undefined, bukan promise.

Sebaliknya, hal yang bergantung pada HTTP sungguhan, misalnya header apa yang benar-benar dikirim server yang sudah di-deploy, tidak cocok dites begini. Repo ini menyimpan pengecekan semacam itu di skrip terpisah yang menembak situs yang berjalan.

Langkah 5: Jalankan sebagian tes dan lihat cara repo ini melakukannya

Saat tesmu mulai banyak, jalankan sebagian saja dengan --test-name-pattern. Polanya diartikan sebagai regular expression JavaScript dan dicocokkan ke nama tes. Ada juga --watch untuk menjalankan ulang saat file berubah, tetapi dokumentasi Node menandainya masih experimental.

# Only tests whose name matches the pattern (a JavaScript regular expression)
node --test --test-name-pattern="methodNotAllowed"

# Or pass a glob yourself instead of relying on the default patterns
node --test "tests/**/*.test.js"

# This site's package.json: one script, the whole tests/ folder
# "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test tests/"

Di repo situs ini, script test memanggil node --test pada folder tests/, dan isinya file seperti agent-readiness.test.mjs. Tes yang mengimpor modul TypeScript memuat resolver kecil lebih dulu, dan header filenya mencatat bahwa itu butuh Node 22.18 atau lebih baru. Dokumentasi Node mencatat type stripping aktif secara default sejak v22.18.0. Untuk tes JavaScript biasa seperti di artikel ini, Node 20 sudah cukup.

Langkah 6: Jadikan npm test gerbang di GitHub Actions

Tes yang hanya jalan kalau kamu ingat menjalankannya akan terlewat. Pasang sebagai langkah workflow sebelum build, jadi pull request dengan tes merah tidak pernah sampai ke tahap build atau deploy. Langkah run gagal kalau exit code-nya bukan nol, itulah seluruh mekanismenya.

# .github/workflows/test.yml
name: test

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci

      # Exit code 1 from node --test fails this step and stops the job
      - run: npm test

      # Only reached when the tests passed
      - run: npm run build

Untuk perbandingan, job verify di workflow deploy situs ini menjalankan npx tsc --noEmit dan npm run lint, lalu job build menunggunya lewat needs. Menambah npm test di antara keduanya adalah satu baris yang sama bentuknya.

Belum punya workflow? Ikuti dulu cara membuat CI/CD GitHub Actions untuk pemula, lalu kembali ke sini untuk menambahkan langkah tes.

Tes yang namanya tidak cocok pola tidak error, ia hanya tidak dijalankan. Kalau jumlah tests di output lebih sedikit dari yang kamu kira, periksa nama file dulu sebelum mencurigai kodenya.

Error umum dan cara memperbaikinya

Enam masalah yang paling sering muncul saat pertama kali memakai node:test, dengan penyebab dan perbaikannya.

GejalaPenyebabPerbaikan
ERR_MODULE_NOT_FOUND: Cannot find module .../lib/slugify, exit code 1Import relatif di ES module tanpa ekstensi file.Tulis lengkap: import dari ../lib/slugify.js, bukan ../lib/slugify.
Jumlah tests lebih sedikit dari seharusnya, tanpa errorNama file tidak cocok pola, misalnya tests/slugify.js, sehingga tidak pernah dijalankan.Ganti nama menjadi slugify.test.js, atau berikan pola glob sebagai argumen node --test.
Tes async gagal dengan actual bernilai undefinedLupa await pada response.json(), jadi yang dibandingkan adalah promise, bukan isinya.Jadikan tes async dan tulis await response.json().
assert.equal lolos padahal 1 dibandingkan dengan teks 1Impor dari node:assert biasa memakai mode legacy yang membandingkan dengan ==.Impor dari node:assert/strict, di sana assert.equal adalah alias strictEqual.
Tes lulus di komputermu tetapi gagal saat mengimpor file TypeScript di Node lamaNode di bawah 22.18 tidak membuka type stripping secara default.Naikkan versi Node di komputer dan workflow, atau tulis tes untuk file JavaScript biasa.
Pipeline hijau padahal tes tidak pernah dijalankanWorkflow tidak punya langkah npm test, atau script test di package.json tidak ada.Tambahkan run: npm test sebelum build dan pastikan script test memanggil node --test.

Checklist sebelum push

  • File tes berakhiran .test.js atau .test.mjs dan muncul di jumlah tests.
  • Satu tes sudah kamu rusak dengan sengaja dan kamu melihat exit code 1.
  • Tes async memakai await untuk setiap promise.
  • assert diimpor dari node:assert/strict.
  • Workflow menjalankan npm ci, lalu npm test, baru npm run build.

Belajar lebih dalam

Pelajaran gratis Tes Situs Next.js dengan node:test dan Build Gate menjelaskan kapan sebuah aturan jadi tes dan kapan jadi build gate yang menggagalkan build. Untuk gambaran pipeline lengkap sampai deploy, baca CI/CD dengan GitHub Actions: dari nol sampai produksi, dan untuk menguji app hasil build di browser sungguhan, lihat cara membuat smoke test Playwright di GitHub Actions.

Aturan yang bisa kamu bawa: mulai dari fungsi murni, tulis kasus sebagai tabel data, rusak satu nilai untuk memastikan tesnya benar-benar bisa gagal, lalu pasang npm test sebelum build supaya exit code 1 yang menjaga produksi, bukan ingatanmu.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apakah bisa menjalankan tes di Next.js tanpa Jest atau Vitest?

Bisa. Node.js membawa test runner bawaan yang ditandai stable sejak v20.0.0 di dokumentasinya. Kamu menulis file bernama seperti *.test.js yang mengimpor describe dan it dari node:test, lalu menjalankan node --test. Paling cocok untuk fungsi murni dan handler yang bisa dipanggil langsung.

Kenapa node --test tidak menemukan file tes saya?

Tanpa argumen, Node hanya menjalankan file yang cocok dengan pola default, seperti *.test.js, *-test.js, test-*.js, atau file di folder bernama test. File bernama tests/slugify.js tidak cocok dengan satu pun, jadi dilewati tanpa pesan. Ganti nama menjadi slugify.test.js atau berikan pola glob.

Kenapa tes async saya gagal dengan actual undefined?

Kemungkinan besar kamu lupa await pada sebuah promise, misalnya response.json(). Assertion lalu membandingkan promise, bukan isi body yang sudah di-parse. Jadikan fungsi tes async dan tulis await sebelum pemanggilannya.

Bagaimana cara menguji route handler API di Next.js?

Pindahkan logikanya ke fungsi yang mengembalikan Response, lalu panggil langsung di tes dan periksa status, header, dan body. Kamu tidak perlu menyalakan server sama sekali. Pengecekan yang butuh perilaku HTTP sungguhan, seperti header dari server yang sudah di-deploy, sebaiknya di skrip terpisah.

Bagaimana membuat GitHub Actions gagal saat ada tes yang gagal?

Tambahkan langkah run npm test setelah npm ci dan sebelum build. node --test menetapkan exit code 1 saat ada tes gagal, dan langkah workflow dengan exit code bukan nol menggagalkan job. Job berikutnya yang bergantung lewat needs pun tidak akan berjalan.