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 #
| Situasi | Sikap |
|---|---|
| Panggilan ke layanan eksternal yang berbayar atau lambat | Mock di batas sistem |
| Jam sistem dan penjadwalan waktu | Mock atau fake timer |
| Angka acak yang menentukan hasil | Suntikkan generator yang bisa dikendalikan |
Function murni seperti hitungDiskon | Tidak perlu mock — langsung uji |
| Database lokal yang cepat dan bisa dibangun ulang | Lebih sehat memakai instance sungguhan yang kecil |
| Perilaku internal object pribadi milik module sendiri | Hindari 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:
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);
});node --test hitungan.test.mjsOutput yang Dihasilkan:
✔ mock.fn mencatat setiap panggilan
✔ perilaku tiruan bisa diganti
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0Riwayat panggilan adalah fitur yang membedakan mock.fn dari function biasa:
- Baris 6-8: function tiruan dibuat dari arrow function
(a, b) => a + b, lalu dipanggil dua kali seperti function mana pun. - Baris 10 (
callCount()): method pada propertymockyang menghitung seluruh panggilan — dua panggilan berarti nilai2. - Baris 11 (
calls[0].arguments): argumen panggilan pertama tersimpan sebagai array[2, 3], siap dibandingkan dengandeepStrictEqual. - Baris 12 (
calls[1].result): nilai balik setiap panggilan juga direkam — panggilan kedua menghasilkan30. - Baris 18 (
mockImplementation): perilaku tiruan dapat diganti setelah function dibuat, berguna untuk mensimulasikan perubahan kondisi di tengah test — stok yang mulanya100lalu habis menjadi0.
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:
export const pengirimPesan = {
kirim(tujuan, isi) {
// di dunia nyata: permintaan HTTP ke penyedia layanan pesan
return `terkirim ke ${tujuan}: ${isi}`;
},
};export function bagikanBerita(pengirim, daftarTujuan, judul) {
const laporan = [];
for (const tujuan of daftarTujuan) {
const status = pengirim.kirim(tujuan, judul);
laporan.push(status);
}
return laporan;
}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'
);
});node --test layanan.test.mjsOutput yang Dihasilkan:
✔ 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 0Tiga keputusan desain pada berkas-berkas di atas saling menopang:
layanan.mjsbaris 1:bagikanBeritamenerimapengirimsebagai parameter alih-alih mengimpor sendiri — pola dependency injection yang membuat mock bisa masuk tanpa trik.- Baris 9 pada test:
t.mock.method(...)menggantipengirimPesan.kirimdengan tiruan yang selalu mengembalikan'ok'; seluruh permintaan kini berhenti di memori proses. - Baris 18-21: dua pemeriksaan berjalan berdampingan — hasil
laporanyang benar dan bukti bahwakirimterpanggil dua kali dengan argumen pertama yang benar. Jika suatu haribagikanBeritalupa memanggil pengirim, pemeriksaan kedua inilah yang gagal. - 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:
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:
dicatat
terpanggil 1 kali
haloTanpa 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.