Membangun kebiasaan testing menjadi berat jika langkah pertamanya adalah memilih package, membandingkan framework, dan menyetel konfigurasi. Node.js menghapus alasan tersebut sejak versi 18: runtime ini sudah membawa test runner sendiri bernama node:test — tanpa instalasi, tanpa konfigurasi, dan mengikuti kita ke proyek mana pun. Pada Chapter ini kita mengenal seluruh perangkat dasar runner tersebut: mendeklarasikan test dengan test(), mengelompokkannya dengan describe() dan it(), menyusun subtest, menandai test yang belum siap, dan mengatur kode persiapan lewat hook.

Test Pertama dengan node:test #

Test runner ini dipakai lewat dua module: node:test menyediakan function test untuk mendeklarasikan kasus uji, sementara node:assert menyediakan pembanding hasil. Kode yang diuji kita letakkan di module terpisah agar test dan logika tidak bercampur. Perhatikan dua berkas berikut:

js
export function tambah(a, b) {
  return a + b;
}

export function bagi(a, b) {
  if (b === 0) {
    throw new Error('pembagi tidak boleh nol');
  }
  return a / b;
}
js
import { test } from 'node:test';
import assert from 'node:assert';
import { tambah, bagi } from './kalkulator.mjs';

test('tambah menjumlahkan dua angka', () => {
  assert.strictEqual(tambah(2, 3), 5);
});

test('bagi membagi angka pertama dengan angka kedua', () => {
  assert.strictEqual(bagi(10, 4), 2.5);
});

Jalankan berkas test tersebut secara eksplisit:

bash
node --test dasar.test.mjs

Output yang Dihasilkan:

text
✔ tambah menjumlahkan dua angka
✔ bagi membagi angka pertama dengan angka kedua
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Tanda ✔ menandai test yang lulus, sedangkan rangkuman di bawahnya menghitung jumlah test, suite, dan hasilnya. Daftar lengkap opsi test() dapat dibaca pada dokumentasi node:test . Mari kita bedah bagian-bagian penting berkas test di atas:

  1. Baris 1-2: kedua module inti testing diimpor dengan prefix node: — konvensi yang juga dipakai chapter-chapter sebelumnya untuk modul bawaan.
  2. Baris 3: function dari kalkulator.mjs diimpor sebagaimana module biasa; runner tidak menuntut bentuk khusus dari kode yang diuji.
  3. Baris 5 dan 9: setiap panggilan test() mendaftarkan satu kasus uji dengan nama yang menjelaskan perilaku yang dijamin — bukan nama function yang dijalankan.
  4. Baris 6 dan 10: assert.strictEqual(nilaiNyata, nilaiHarapan) membandingkan dengan perbandingan ketat ===; ketidakcocokan apa pun membuat test gagal.

Tiga Cara Menjalankan Test #

PerintahPerilaku
node --test dasar.test.mjsMenjalankan berkas yang disebut saja
node dasar.test.mjsMenjalankan berkas itu juga — tanpa flag --test, Node tetap mendeteksi module node:test dan memakai runner yang sama
node --testMenemukan dan menjalankan semua berkas test di folder kerja beserta subfolder-nya

Cara kedua berguna saat kita sedang fokus pada satu berkas; hasilnya identik dengan pemanggilan melalui --test:

bash
node dasar.test.mjs

Output yang Dihasilkan:

text
✔ tambah menjumlahkan dua angka
✔ bagi membagi angka pertama dengan angka kedua
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Cara ketiga adalah bentuk yang paling sering kita pakai sehari-hari. Tanpa argumen, test runner mencari semua berkas yang cocok dengan pola berikut: *.test.mjs, *.test.js, *.test.cjs, *-test.*, *_test.*, test-*, test.*, serta semua berkas JavaScript di dalam folder test/; daftar polanya tercantum pada dokumentasi test runner . Kita akan melihat hasilnya utuh pada bagian terakhir chapter ini, setelah seluruh berkas contohnya selesai dibahas.

Mengelompokkan dengan describe dan it #

Ketika jumlah test bertambah, daftar datar menjadi sulit dipindai. Function describe() mengelompokkan test yang menguji fitur yang sama, dan di dalamnya kita boleh memakai it() — nama lain dari test() yang lazim dipakai berpasangan dengan describe:

js
import { describe, it, test } from 'node:test';
import assert from 'node:assert';
import { tambah, bagi } from './kalkulator.mjs';

describe('penjumlahan', () => {
  it('menjumlahkan dua angka positif', () => {
    assert.strictEqual(tambah(20, 22), 42);
  });

  it('menjumlahkan angka negatif', () => {
    assert.strictEqual(tambah(-5, 3), -2);
  });
});

describe('pembagian', () => {
  it('membagi dua angka', () => {
    assert.strictEqual(bagi(10, 4), 2.5);
  });

  it('menolak pembagi nol', () => {
    assert.throws(() => bagi(5, 0), { message: 'pembagi tidak boleh nol' });
  });

  test('subtest memeriksa beberapa kasus sekaligus', async (t) => {
    await t.test('10 dibagi 2', () => {
      assert.strictEqual(bagi(10, 2), 5);
    });
    await t.test('9 dibagi 3', () => {
      assert.strictEqual(bagi(9, 3), 3);
    });
  });
});
bash
node --test berkelompok.test.mjs

Output yang Dihasilkan:

text
▶ penjumlahan
  ✔ menjumlahkan dua angka positif
  ✔ menjumlahkan angka negatif
✔ penjumlahan
▶ pembagian
  ✔ membagi dua angka
  ✔ menolak pembagi nol
  ▶ subtest memeriksa beberapa kasus sekaligus
    ✔ 10 dibagi 2
    ✔ 9 dibagi 3
  ✔ subtest memeriksa beberapa kasus sekaligus
✔ pembagian
ℹ tests 7
ℹ suites 2
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Tanda ▶ menandai suite yang dibuka dan ditutup dengan nama describe-nya. Dua hal baru pada berkas ini perlu dibedah:

  1. Baris 5 dan 14: describe(nama, callback) membentuk suite; setiap it() di dalamnya menjadi bagian dari nama lengkap test, misalnya pembagian > menolak pembagi nol.
  2. Baris 23-29 (subtest): object t — context yang diterima callback test — memiliki method t.test() untuk mendaftarkan subtest. Subtest berguna ketika satu perilaku layak diperiksa lewat banyak pasangan masukan: setiap subtest dilaporkan terpisah sehingga kita tahu persis kasus mana yang gagal.
  3. Kata kunci async dan await: t.test() mengembalikan promise, sehingga kita menunggu setiap subtest selesai sebelum mendaftarkan yang berikutnya — materi async dalam testing dibahas tuntas pada Chapter Menguji Kode Asynchronous.

Melewatkan Test dan Menandai Todo #

Dua penanda pada object opsi membantu mengelola suite yang sedang setengah jalan. Opsi skip mencegah runner menjalankan isi test sama sekali — isi tersebut bisa berisi assertion yang pasti gagal sekalipun. Opsi todo menandai pekerjaan yang belum dikerjakan tanpa memerlukan isi test:

js
import { test } from 'node:test';
import assert from 'node:assert';

test('kalkulasi dasar tetap berjalan', () => {
  assert.strictEqual(1 + 1, 2);
});

test('kasus khusus angka negatif', { skip: 'menunggu perbaikan bug #12' }, () => {
  assert.ok(false);
});

test('perilaku bilangan desimal', { todo: 'dilengkapi pada rilis berikutnya' });
bash
node --test penanda.test.mjs

Output yang Dihasilkan:

text
✔ kalkulasi dasar tetap berjalan
﹣ kasus khusus angka negatif # menunggu perbaikan bug #12
✔ perilaku bilangan desimal # dilengkapi pada rilis berikutnya
ℹ tests 3
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1

Simbol ﹣ menandai test yang dilewati beserta alasannya, sedangkan test bertanda todo ditampilkan dengan ✔ dan dicatat pada baris rangkuman todo. Kedua penanda ini didokumentasikan pada bagian skipping tests dan todo tests . Perhatikan bahwa assert.ok(false) pada test kedua tidak pernah dieksekusi — itulah bukti bahwa skip benar-benar mencegah isi test berjalan. Alasan yang tertulis (menunggu perbaikan bug #12) ikut tercetak sehingga rekan tim tahu mengapa test tersebut diam.

Menyaring Test yang Dijalankan #

Saat mendiagnosis satu kegagalan, kita tidak perlu menjalankan seluruh suite. Flag --test-name-pattern menerima regular expression dan hanya menjalankan test yang namanya cocok — termasuk seluruh isi suite yang namanya cocok:

bash
node --test --test-name-pattern=pembagian berkelompok.test.mjs

Output yang Dihasilkan:

text
▶ pembagian
  ✔ membagi dua angka
  ✔ menolak pembagi nol
  ▶ subtest memeriksa beberapa kasus sekaligus
    ✔ 10 dibagi 2
    ✔ 9 dibagi 3
  ✔ subtest memeriksa beberapa kasus sekaligus
✔ pembagian
ℹ tests 5
ℹ suites 1
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Seluruh suite penjumlahan tidak muncul pada output karena namanya tidak memuat kata pembagian. Pola ini berlaku terhadap nama lengkap test, sehingga --test-name-pattern="menolak" juga akan menangkap test menolak pembagi nol di dalam suite mana pun.

Hook: Kode Sebelum dan Sesudah Test #

Kode persiapan yang sama sering dibutuhkan banyak test — menyiapkan data, membuka koneksi, lalu membersihkannya. Test runner menyediakan empat hook: before berjalan sekali sebelum seluruh suite, after sekali setelahnya, sementara beforeEach dan afterEach mengelilingi setiap test. Berkas berikut membuktikan urutannya dengan merekam jejak eksekusi:

js
import { test, describe, before, after, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';

const jejak = [];

describe('kumpulan dengan hook', () => {
  before(() => jejak.push('before'));
  after(() => jejak.push('after'));
  beforeEach(() => jejak.push('beforeEach'));
  afterEach(() => jejak.push('afterEach'));

  test('test pertama', () => jejak.push('test-1'));
  test('test kedua', () => jejak.push('test-2'));
});

test('urutan eksekusi lengkap', () => {
  assert.deepStrictEqual(jejak, [
    'before',
    'beforeEach', 'test-1', 'afterEach',
    'beforeEach', 'test-2', 'afterEach',
    'after',
  ]);
});
bash
node --test urutan-hook.test.mjs

Output yang Dihasilkan:

text
▶ kumpulan dengan hook
  ✔ test pertama
  ✔ test kedua
✔ kumpulan dengan hook
✔ urutan eksekusi lengkap
ℹ tests 3
ℹ suites 1
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Test terakhir di luar suite memeriksa isi jejak setelah seluruh suite selesai — dan lulus, yang berarti urutan sesungguhnya memang before, lalu pasangan beforeEach-test-afterEach untuk setiap test, lalu after tepat satu kali. Hook ini akan kembali dipakai pada Chapter Integration Test untuk HTTP Server untuk menyalakan dan mematikan server uji.

Menjalankan Seluruh Suite Sekaligus #

Sekarang keempat berkas contoh pada chapter ini telah lengkap, dan inilah waktunya melihat cara ketiga dari tabel di awal: node --test tanpa argumen menemukan semuanya secara otomatis.

bash
node --test

Output yang Dihasilkan:

text
▶ penjumlahan
  ✔ menjumlahkan dua angka positif
  ✔ menjumlahkan angka negatif
✔ penjumlahan
▶ pembagian
  ✔ membagi dua angka
  ✔ menolak pembagi nol
  ▶ subtest memeriksa beberapa kasus sekaligus
    ✔ 10 dibagi 2
    ✔ 9 dibagi 3
  ✔ subtest memeriksa beberapa kasus sekaligus
✔ pembagian
✔ tambah menjumlahkan dua angka
✔ bagi membagi angka pertama dengan angka kedua
✔ kalkulasi dasar tetap berjalan
﹣ kasus khusus angka negatif # menunggu perbaikan bug #12
✔ perilaku bilangan desimal # dilengkapi pada rilis berikutnya
▶ kumpulan dengan hook
  ✔ test pertama
  ✔ test kedua
✔ kumpulan dengan hook
✔ urutan eksekusi lengkap
ℹ tests 15
ℹ suites 3
ℹ pass 13
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1

Rangkuman melaporkan 15 test dari empat berkas: tiga belas lulus, satu dilewati, satu bertanda todo. Angka-angka inilah yang nantinya dipantau otomatis oleh pipeline CI — perintah yang sama berjalan identis di komputer lokal dan di server build, dan kode keluar bukan nol akan menghentikan pipeline.

Ke Mana Setelah Ini #

Kita kini dapat mendeklarasikan test, mengelompokkannya, menyaringnya, dan mengaturnya dengan hook memakai node:test semata. Satu perangkat dari contoh-contoh di atas masih sengaja ditunda pembahasannya: assert.strictEqual beserta saudara-saudaranya dari module node:assert. Chapter Assertion dengan node:assert membedah pilihan assertion tersebut satu per satu — termasuk jebakan klasik antara equal dan strictEqual.