Unit test yang ideal hanya menguji satu potong kode dalam isolation penuh — tetapi kode nyata hampir selalu bersandar pada sesuatu di luar dirinya: layanan pesan, database, jam sistem, angka acak. Dependensi tersebut membuat test lambat, mahal, atau hasilnya berubah-ubah tanpa kode kita berubah. Mock adalah panggung palsu untuk dependensi itu: object atau function tiruan yang perilakunya kita tentukan dan setiap panggilannya tercatat. Pada Chapter ini kita memakai perangkat mock bawaan node:test — mock.fn, mock.method, dan pelacak t.mock — sekaligus belajar kapan mock justru menjadi beban.

Masalahnya: Dependensi yang Mengganggu #

Bayangkan function bagikanBerita yang mengirim berita ke daftar alamat email lewat layanan pihak ketiga. Menguji function tersebut dengan pengirim sungguhan berarti setiap kali suite berjalan, layanan pihak ketiga menerima permintaan sungguhan — lengkap dengan kuota, biaya, dan kemungkinan gagal karena jaringan. Test kita berhenti menguji logika dan mulai menguji dunia luar. Tiga kategori dependensi paling sering mengganggu: layanan eksternal (jaringan, API berbayar), sumber ketidakpastian (jam, angka acak), dan keadaan yang sulit dibangun (database besar, berkas sistem penuh).

Mock menjawab ketiganya dengan mengganti dependensi pada batas sistem: test menyuntikkan tiruan yang murah dan terprediksi, logika kita berjalan seperti biasa, lalu test memeriksa dua hal — nilai balik yang benar dan panggilan yang benar. Dua jenis pemeriksaan inilah yang membedakan mock dari assertion biasa.

Kapan Memakai Mock dan Kapan Tidak #

SituasiSikap
Panggilan ke layanan eksternal yang berbayar atau lambatMock di batas sistem
Jam sistem dan penjadwalan waktuMock atau fake timer
Angka acak yang menentukan hasilSuntikkan generator yang bisa dikendalikan
Function murni seperti hitungDiskonTidak perlu mock — langsung uji
Database lokal yang cepat dan bisa dibangun ulangLebih sehat memakai instance sungguhan yang kecil
Perilaku internal object pribadi milik module sendiriHindari mock — uji lewat perilaku yang terlihat

Baris terakhir tabel adalah yang paling sering dilanggar. Mock yang menelusuri detail internal kode sendiri membuat test rapuh: setiap refactoring tak penting memaksa test ikut diubah, padahal perilaku tidak berubah. Aturan praktisnya: mock dependensi di batas sistem kita, percayai perilaku internal lewat hasil yang terlihat dari luar.

mock.fn: Function Palsu yang Mencatat #

mock.fn membuat function tiruan dari implementasi apa pun yang kita berikan. Function tiruan ini bisa dipanggil seperti biasa — hasilnya keluar dari implementasi yang kita tulis — dan setiap panggilannya tercatat pada property mock:

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

test('mock.fn mencatat setiap panggilan', () => {
  const penambah = mock.fn((a, b) => a + b);

  penambah(2, 3);
  penambah(10, 20);

  assert.strictEqual(penambah.mock.callCount(), 2);
  assert.deepStrictEqual(penambah.mock.calls[0].arguments, [2, 3]);
  assert.strictEqual(penambah.mock.calls[1].result, 30);
});

test('perilaku tiruan bisa diganti', () => {
  const ambilStok = mock.fn(() => 100);
  assert.strictEqual(ambilStok(), 100);

  ambilStok.mock.mockImplementation(() => 0);
  assert.strictEqual(ambilStok(), 0);
});
bash
node --test hitungan.test.mjs

Output yang Dihasilkan:

text
✔ mock.fn mencatat setiap panggilan
✔ perilaku tiruan bisa diganti
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Riwayat panggilan adalah fitur yang membedakan mock.fn dari function biasa:

  1. Baris 6-8: function tiruan dibuat dari arrow function (a, b) => a + b, lalu dipanggil dua kali seperti function mana pun.
  2. Baris 10 (callCount()): method pada property mock yang menghitung seluruh panggilan — dua panggilan berarti nilai 2.
  3. Baris 11 (calls[0].arguments): argumen panggilan pertama tersimpan sebagai array [2, 3], siap dibandingkan dengan deepStrictEqual.
  4. Baris 12 (calls[1].result): nilai balik setiap panggilan juga direkam — panggilan kedua menghasilkan 30.
  5. Baris 18 (mockImplementation): perilaku tiruan dapat diganti setelah function dibuat, berguna untuk mensimulasikan perubahan kondisi di tengah test — stok yang mulanya 100 lalu habis menjadi 0.

mock.method: Mengganti Method pada Object #

Kebutuhan yang lebih umum bukan mengganti function lepas, melainkan method yang menempel pada object — pengirimPesan.kirim, console.log, atau method client database. mock.method(object, 'namaMethod', implementasi) mengganti method tersebut pada object aslinya dan mengembalikan pengawas:

js
export const pengirimPesan = {
  kirim(tujuan, isi) {
    // di dunia nyata: permintaan HTTP ke penyedia layanan pesan
    return `terkirim ke ${tujuan}: ${isi}`;
  },
};
js
export function bagikanBerita(pengirim, daftarTujuan, judul) {
  const laporan = [];
  for (const tujuan of daftarTujuan) {
    const status = pengirim.kirim(tujuan, judul);
    laporan.push(status);
  }
  return laporan;
}
js
import { test } from 'node:test';
import assert from 'node:assert';
import { pengirimPesan } from './pengirim-pesan.mjs';
import { bagikanBerita } from './layanan.mjs';

test('bagikanBerita mengirim ke setiap tujuan', (t) => {
  const tiruan = t.mock.method(pengirimPesan, 'kirim', () => 'ok');

  const laporan = bagikanBerita(
    pengirimPesan,
    ['[email protected]', '[email protected]'],
    'Rilis Node baru'
  );

  assert.deepStrictEqual(laporan, ['ok', 'ok']);
  assert.strictEqual(tiruan.mock.callCount(), 2);
  assert.deepStrictEqual(tiruan.mock.calls[0].arguments, [
    '[email protected]',
    'Rilis Node baru',
  ]);
});

test('pengirim asli kembali utuh setelah test selesai', () => {
  assert.strictEqual(
    pengirimPesan.kirim('[email protected]', 'cek koneksi'),
    'terkirim ke [email protected]: cek koneksi'
  );
});
bash
node --test layanan.test.mjs

Output yang Dihasilkan:

text
✔ bagikanBerita mengirim ke setiap tujuan
✔ pengirim asli kembali utuh setelah test selesai
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Tiga keputusan desain pada berkas-berkas di atas saling menopang:

  1. layanan.mjs baris 1: bagikanBerita menerima pengirim sebagai parameter alih-alih mengimpor sendiri — pola dependency injection yang membuat mock bisa masuk tanpa trik.
  2. Baris 9 pada test: t.mock.method(...) mengganti pengirimPesan.kirim dengan tiruan yang selalu mengembalikan 'ok'; seluruh permintaan kini berhenti di memori proses.
  3. Baris 18-21: dua pemeriksaan berjalan berdampingan — hasil laporan yang benar dan bukti bahwa kirim terpanggil dua kali dengan argumen pertama yang benar. Jika suatu hari bagikanBerita lupa memanggil pengirim, pemeriksaan kedua inilah yang gagal.
  4. Test kedua: memastikan method asli kembali utuh — pengirim sungguhan menjawab lagi setelah test pertama selesai, bukti bahwa tiruan sudah dibersihkan.

Perhatikan bahwa test pertama memakai t.mock.method, bukan mock.method langsung. Object t yang diterima callback test membawa pelacak mock sendiri: semua mock yang dibuat lewat t.mock dipulihkan otomatis saat test berakhir — itulah sebabnya test kedua melihat method asli. Jika kita memakai mock.method global, pemulihan menjadi tugas kita:

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

const layar = { tulis(data) { return data; } };
const pengawas = mock.method(layar, 'tulis', () => 'dicatat');

console.log(layar.tulis('halo'));
console.log('terpanggil', pengawas.mock.callCount(), 'kali');

pengawas.mock.restore();
console.log(layar.tulis('halo'));

Output yang Dihasilkan:

text
dicatat
terpanggil 1 kali
halo

Tanpa pengawas.mock.restore() di baris kedua dari bawah, layar.tulis tetap menjadi tiruan sampai proses berakhir — dan test lain yang memakai object yang sama akan dikejutkan oleh perilaku palsu.

Ke Mana Setelah Ini #

Dengan mock.fn untuk function lepas, mock.method untuk method object, dan t.mock untuk pemulihan otomatis, kita dapat mengisolasi unit dari dependensi yang mengganggu. Suite test kini cepat dan menentu — dan pertanyaan berikutnya muncul alami: seberapa besar bagian kode kita yang benar-benar sudah tersentuh test? Chapter Test Coverage mengukurnya dengan laporan cakupan bawaan Node.js.