Uji tanpa asersi hanyalah kode yang berjalan tanpa keluhan — ia tidak pernah membenarkan apa pun. Kita bisa memanggil totalKeranjang([]) dan tidak terjadi galat, padahal hasilnya salah. Asersi (assertion) adalah klaim tertulis di dalam uji: “nilai ini harus sama dengan nilai itu”, “kode ini harus melempar galat”. Ketika klaim tidak terpenuhi, asersi melempar AssertionError, dan itulah satu-satunya bahasa yang dipahami test runner untuk menyatakan gagal.

Chapter ini membedah lima asersi node:assert/strict yang akan paling sering kita pakai: strictEqual, deepStrictEqual, ok, throws, dan match. Selain bentuk yang lulus, kita juga melihat pesan kegagalannya apa adanya — karena membaca pesan kegagalan adalah keterampilan yang dipakai jauh lebih sering daripada menulis ujinya.

Berkas uji berikut memuat kelima asersi dalam satu rangkaian tentang keranjang belanja:

import { test } from 'node:test';
import assert from 'node:assert/strict';

function formatRupiah(angka) {
  return `Rp${angka.toLocaleString('id-ID')}`;
}

function ringkasItem(item) {
  return {
    nama: item.nama,
    jumlah: item.jumlah,
    subtotal: item.harga * item.jumlah,
  };
}

const KUPON_DIKENAL = new Set(['HEMAT10', 'MEMBER15']);

function terapkanKupon(keranjang, kode) {
  if (!KUPON_DIKENAL.has(kode)) {
    throw new RangeError(`kode kupon tidak dikenal: ${kode}`);
  }
  return keranjang;
}

test('formatRupiah menambahkan pemisah ribuan', () => {
  assert.strictEqual(formatRupiah(1250000), 'Rp1.250.000');
});

test('ringkasItem mengembalikan object dengan struktur lengkap', () => {
  const hasil = ringkasItem({ nama: 'Buku Algoritma', harga: 85000, jumlah: 2 });

  assert.deepStrictEqual(hasil, {
    nama: 'Buku Algoritma',
    jumlah: 2,
    subtotal: 170000,
  });
});

test('keranjang hasil kupon HEMAT10 tidak kosong', () => {
  const keranjang = terapkanKupon([{ nama: 'Pulpen Gel', harga: 12000, jumlah: 1 }], 'HEMAT10');

  assert.ok(Array.isArray(keranjang));
  assert.ok(keranjang.length > 0, 'keranjang tidak boleh kosong');
});

test('kode kupon tidak dikenal melempar RangeError', () => {
  assert.throws(() => terapkanKupon([], 'DISKON99'), /kode kupon tidak dikenal: DISKON99/);
});

test('email pembeli berformat benar', () => {
  assert.match('[email protected]', /^[^@\s]+@[^@\s]+\.[^@\s]+$/);
});

Output yang Dihasilkan:

text
✔ formatRupiah menambahkan pemisah ribuan (12.078131ms)
✔ ringkasItem mengembalikan object dengan struktur lengkap (0.713688ms)
✔ keranjang hasil kupon HEMAT10 tidak kosong (0.166362ms)
✔ kode kupon tidak dikenal melempar RangeError (0.354035ms)
✔ email pembeli berformat benar (0.239008ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 67.476999

strictEqual: Identitas Penuh Dua Nilai #

assert.strictEqual(nyata, diharapkan) menuntut dua nilai identik dengan aturan ===: tipe dan nilai keduanya harus sama. Baris 26 menggunakannya untuk hasil fungsi berupa string — bentuk pemakaian yang paling lazim.

Pesan kegagalannya dirancang untuk menunjukkan ketidakcocokan sekecil apa pun. Mari kita pancing beberapa varian gagal di terminal:

js
import assert from 'node:assert/strict';

function tampilkanKegagalan(fn) {
  try {
    fn();
  } catch (err) {
    console.log(err.message);
  }
}

tampilkanKegagalan(() => assert.strictEqual(5, 7));
tampilkanKegagalan(() => assert.strictEqual(1, '1'));
tampilkanKegagalan(() => assert.strictEqual({ a: 1 }, { a: 1 }));

Output yang Dihasilkan:

text
Expected values to be strictly equal:

5 !== 7

Expected values to be strictly equal:

1 !== '1'

Values have same structure but are not reference-equal:

{
  a: 1
}

Tiga pesan itu mencatat tiga aturan sekaligus. Kasus pertama, 5 !== 7, adalah kegagalan nilai biasa. Kasus kedua menegaskan mode strict: angka 1 dan string '1' tidak dianggap sama, sehingga perbedaan tipe tertangkap. Kasus ketiga paling sering mengejutkan pemula: dua object dengan isi persis sama pun dianggap berbeda, karena strictEqual membandingkan referensi — dua object berbeda selalu dua alamat memori berbeda. Untuk membandingkan isi object, kita butuh asersi berikutnya.

deepStrictEqual: Membandingkan Isi Struktur #

assert.deepStrictEqual(nyata, diharapkan) membedah struktur nilai secara menyeluruh: setiap properti object, setiap elemen array, sampai tipe tiap daunnya, semuanya harus sama. Baris 32–36 memakainya untuk memastikan ringkasItem menghasilkan object lengkap — bukan sekadar salah satu propertinya.

Bagian terbaik dari asersi ini adalah pesan kegagalannya berbentuk diff, mirip hasil pembanding berkas. Kita uji dengan nilai yang sedikit salah:

js
import assert from 'node:assert/strict';

const nyata = { nama: 'Buku Algoritma', jumlah: 2, subtotal: 170000 };
const diharapkan = { nama: 'Buku Algoritma', jumlah: 3, subtotal: 170000 };

try {
  assert.deepStrictEqual(nyata, diharapkan);
} catch (err) {
  console.log(err.message);
}

Output yang Dihasilkan:

text
Expected values to be strictly deep-equal:
+ actual - expected

  {
+   jumlah: 2,
-   jumlah: 3,
    nama: 'Buku Algoritma',
    subtotal: 170000
  }

Tanda + menunjukkan isi dari nilai nyata dan tanda - isi nilai yang diharapkan; satu-satunya baris yang berbeda langsung terlihat. Membaca pesan ini urutannya dari atas ke bawah: label kesalahan, petunjuk simbol, lalu peta strukturnya. Baru setelah struktur dipahami kita membaca nilai properti mana yang menyimpang.

ok, throws, dan match #

Tiga asersi melengkapi kebutuhan yang tidak tercakup pembandingan nilai:

  • assert.ok(nilai, pesan) memastikan nilai bersifat truthy. Baris 42–43 memakainya dua kali: memastikan hasil berupa array, lalu memastikan array tidak kosong. Argumen kedua menjadi teks kegagalan sendiri, jauh lebih informatif daripada bawaannya. Bila argumennya falsy, pesan bawaan mengutip kembali baris asersinya: The expression evaluated to a falsy value: diikuti assert.ok('') — persis seperti yang ditulis, sehingga lokasi kegagalannya mudah dicari.
  • assert.throws(fn, pembanding) memastikan pemanggilan fn melempar galat. Bentuk kedua argumennya fleksibel: sebuah RegExp untuk mencocokkan pesan (baris 47), atau sebuah class untuk mencocokkan jenisnya, misalnya assert.throws(() => cekBerat(0), RangeError).
  • assert.match(string, regex) memastikan string cocok dengan pola (baris 51) — cocok untuk memeriksa format seperti email, nomor telepon, atau kode kupon.

Dua pesan kegagalan milik keduanya patut dihafal. Pertama, bila assert.throws diberi fungsi yang justru tidak melempar apa pun:

text
Missing expected exception.

Kedua, bila pembanding jenisnya salah — kode melempar TypeError padahal uji menuntut RangeError:

text
The error is expected to be an instance of "RangeError". Received "TypeError"

Dan bila assert.match gagal, pesannya mengutip pola beserta input yang ditolak:

text
The input did not match the regular expression /@contoh\.id/. Input:

'belanja@contoh'

Membaca Kegagalan dari Mata Runner #

Sejauh ini pesan kegagalan kita bacakan lewat try/catch. Dalam uji sungguhan, AssertionError dilempar begitu saja dan test runner yang menangkapnya. Supaya bentuk laporannya terbiasa, berikut salinan uji yang sengaja dibuat gagal — klaimnya salah, 111000 !== 110000 — lalu dijalankan dengan node --test:

text
✖ total keranjang memperhitungkan pajak (1.167308ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 56.195702

✖ failing tests:

test at gagal.test.mjs:4:1
✖ total keranjang memperhitungkan pajak (1.167308ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  
  111000 !== 110000
  
      at … (lima baris stack trace menunjuk ke baris asersi di gagal.test.mjs serta internal runner) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: 111000,
    expected: 110000,
    operator: 'strictEqual',
    diff: 'simple'
  }

Transkrip di atas diambil dari uji nyata; hanya jalur berkas pada bagian stack trace yang dipangkas agar ringkas, dan angka durasinya berubah-ubah antar mesin. Bagian yang paling berguna ada di objek bagian bawah: actual menyimpan nilai hasil kode, expected nilai yang diharapkan, dan operator nama asersinya. Runner juga menampilkan ✖ failing tests: khusus setelah ringkasan, sehingga semua kegagalan bisa ditinjau ulang tanpa menggulir laporan panjang.

Mode strict pada node:assert #

Setiap contoh di chapter ini mengimpor dari node:assert/strict — dengan akhiran /strict. Akhiran itu bukan dekorasi: modul node:assert polos masih menyediakan method lama yang membandingkan secara longgar, sedangkan mode strict membuat semua nama beralih ke pembandingan ketat. Perbedaannya terlihat pada satu baris:

js
import assert from 'node:assert';
import strict from 'node:assert/strict';

console.log('equal(1, "1") versi polos:', (() => {
  try { assert.equal(1, '1'); return 'lulus'; } catch { return 'gagal'; }
})());

console.log('equal(1, "1") versi strict:', (() => {
  try { strict.equal(1, '1'); return 'lulus'; } catch { return 'gagal'; }
})());

console.log('strict.equal === strict.strictEqual:', strict.equal === strict.strictEqual);

Output yang Dihasilkan:

text
equal(1, "1") versi polos: lulus
equal(1, "1") versi strict: gagal
strict.equal === strict.strictEqual: true

Versi polos menerima 1 dan '1' sebagai “sama” karena memakai perbandingan longgar — persis perangkap tipe yang sudah kita bahas di section operator. Versi strict menolaknya, dan baris ketiga membuktikan bahwa dalam mode strict, nama equal memang hanyalah alias dari strictEqual. Karena uji seharusnya menangkap perbedaan tipe, bukan menyembunyikannya, kebiasaan yang aman adalah selalu mengimpor dari node:assert/strict — seperti yang dipakai seluruh berkas uji di section ini.

Dengan lima asersi ini, hampir semua klaim uji unit bisa ditulis: nilai tunggal, struktur data, sifat truthy, galat yang harus terlempar, dan format teks. Berikutnya kita berhadapan dengan kasus yang tidak bisa diselesaikan asersi saja: kode yang bergantung pada dunia luar.