Node.js adalah runtime untuk kode asynchronous — hampir setiap proyek nyata dipenuhi promise, timer, dan callback. Kabar baiknya: test runner node:test dirancang untuk dunia itu sejak awal, sehingga menguji kode async nyaris tidak membutuhkan trik. Kabar yang perlu diseriusi: beberapa jebakan klasik — lupa await, callback yang tidak pernah dipanggil, timer yang membuat suite lambat — hanya akan tampak jika kita paham bagaimana runner menunggu kode kita selesai. Pada Chapter ini kita menguji promise yang terpenuhi maupun ditolak, membungkus API callback, mempercepat waktu dengan fake timer, dan memasang batas waktu agar test yang menggantung tertangkap.
Test Asynchronous Adalah Promise #
Aturan dasarnya satu kalimat: jika callback test mengembalikan promise, runner menunggu promise tersebut selesai sebelum menilai lulus atau gagal. Karena itu bentuk paling alami dari test async adalah function async — setiap await di dalamnya ikut ditunggu, dan error yang terlempar setelah await langsung menandai test gagal. Mari kita uji module kecil yang menirukan pengambilan data dengan jeda 10 milidetik:
import { setTimeout as tidur } from 'node:timers/promises';
export async function ambilProfil(id) {
await tidur(10);
if (id <= 0) {
throw new Error('id tidak valid');
}
return { id, nama: 'Budi', kota: 'Bandung' };
}import { test } from 'node:test';
import assert from 'node:assert';
import { ambilProfil } from './ambil-data.mjs';
test('mengambil profil pengguna', async () => {
const profil = await ambilProfil(7);
assert.deepStrictEqual(profil, { id: 7, nama: 'Budi', kota: 'Bandung' });
});
test('id tidak valid ditolak dengan error', async () => {
await assert.rejects(ambilProfil(-1), { message: 'id tidak valid' });
});node --test ambil-data.test.mjsOutput yang Dihasilkan:
✔ mengambil profil pengguna
✔ id tidak valid ditolak dengan error
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0Empat bagian pada berkas test tersebut bekerja sama menunggu dunia asynchronous:
- Baris 5 (
async () => {): kata kunciasyncmembuat callback test otomatis mengembalikan promise; runner menunggu promise itu sampai selesai. - Baris 6:
await ambilProfil(7)menunggu jeda 10 milidetik di dalamambil-data.mjs— pada titik ini runner bersabar, bukan menandai test selesai. - Baris 7: assertion berjalan setelah data sungguhan tiba, sehingga yang dibandingkan adalah hasil akhir, bukan promise-nya.
- Baris 11: penolakan diperiksa dengan
assert.rejectsyang mengembalikan promise — wajib di-awaitagar kegagalan tercatat sebagai kegagalan test, bukan penolakan yang mengambang.
Jebakan Klasik: Lupa await #
Kesalahan paling umum dalam test async adalah lupa menulis await sebelum pemanggilan promise. Test berikut sengaja memuat kesalahan itu — tugasPenting() ditolak setelah 5 milidetik, tetapi test tidak menunggunya:
import { test } from 'node:test';
import assert from 'node:assert';
function tugasPenting() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('gagal menyimpan data')), 5);
});
}
test('menjalankan tugas penting', () => {
const janji = tugasPenting();
assert.ok(janji instanceof Promise);
});node --test lupa-await.test.mjs
echo "kode keluar: $?"Output yang Dihasilkan:
✔ menjalankan tugas penting
ℹ Error: Test "menjalankan tugas penting" at lupa-await.test.mjs:10:1 generated asynchronous activity after the test ended. This activity created the error "Error: gagal menyimpan data" and would have caused the test to fail, but instead triggered an unhandledRejection event.
✖ lupa-await.test.mjs
ℹ tests 2
ℹ suites 0
ℹ pass 1
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
✖ failing tests:
test at lupa-await.test.mjs:1:1
✖ lupa-await.test.mjs
'test failed'
kode keluar: 1Perhatikan keanehan pada output: test itu sendiri bertanda ✔, tetapi berkas keseluruhan dinyatakan gagal dengan kode keluar 1. Runner versi modern mendeteksi aktivitas asynchronous yang masih hidup setelah test berakhir — penolakan gagal menyimpan data muncul setelah test selesai, memicu unhandledRejection, lalu runner menandai berkas test tersebut gagal. Penyelamat ini bagus, tetapi jangan bersandar padanya.
Menguji API Gaya Callback #
Banyak module lama — dan beberapa API Node.js hingga hari ini — memakai pola callback (error, hasil) alih-alih promise. Test runner tidak menyediakan mekanisme done khusus ala framework generasi lama; cara yang dianjurkan adalah membungkus callback menjadi promise, sama seperti pola util.promisify yang kita kenal pada Section Asynchronous Runtime. Dibungkus sekali di dalam test, seluruh peralatan async/await langsung tersedia:
export function bacaKonfigurasi(callback) {
setTimeout(() => {
callback(null, { host: '127.0.0.1', port: 3000, mode: 'produksi' });
}, 10);
}
export function simpanKonfigurasi(data, callback) {
if (!data.host) {
setTimeout(() => callback(new Error('host wajib diisi')), 10);
return;
}
setTimeout(() => callback(null, 'tersimpan'), 10);
}import { test } from 'node:test';
import assert from 'node:assert';
import { bacaKonfigurasi, simpanKonfigurasi } from './baca-konfigurasi.mjs';
test('membaca konfigurasi lewat callback', async () => {
const hasil = await new Promise((resolve, reject) => {
bacaKonfigurasi((error, data) => (error ? reject(error) : resolve(data)));
});
assert.deepStrictEqual(hasil, {
host: '127.0.0.1',
port: 3000,
mode: 'produksi',
});
});
test('konfigurasi tanpa host ditolak', async () => {
const penolakan = new Promise((resolve, reject) => {
simpanKonfigurasi({ port: 3000 }, (error, status) =>
error ? reject(error) : resolve(status)
);
});
await assert.rejects(penolakan, { message: 'host wajib diisi' });
});node --test baca-konfigurasi.test.mjsOutput yang Dihasilkan:
✔ membaca konfigurasi lewat callback
✔ konfigurasi tanpa host ditolak
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0Pembungkusan promise pada kedua test layak dibedah karena polanya berulang di mana-mana:
- Baris 6-8 (sukses):
new Promisemenerima callback denganresolvedanreject; callback asli diarahkan —errormembawa promise kereject, data membawanya keresolve. - Baris 17-21 (kegagalan): promise yang sama-sama dibungkus, lalu diperiksa dengan
assert.rejects— penolakan dari callback lama diperlakukan persis seperti penolakan promise modern.
Mengendalikan Waktu dengan Fake Timer #
Menunggu timer sungguhan membuat suite lambat dan rapuh: test yang menunggu 30 menit jelas mustahil, dan test yang menunggu 3 detik pun terasa berat dijalankan ratusan kali. node:test menyertakan fake timer lewat mock.timers: timer global diganti versi palsu yang maju hanya saat kita memerintahkan tick(). Jam Date juga bisa dibekukan pada titik waktu tertentu:
import { test, mock } from 'node:test';
import assert from 'node:assert';
test('menunggu pesan terjadwal tanpa menunggu sungguhan', () => {
mock.timers.enable({ apis: ['setTimeout'] });
const terkirim = [];
setTimeout(() => terkirim.push('pengingat pagi'), 8 * 60 * 60 * 1000);
setTimeout(() => terkirim.push('laporan sore'), 17 * 60 * 60 * 1000);
assert.strictEqual(terkirim.length, 0);
mock.timers.tick(8 * 60 * 60 * 1000);
assert.deepStrictEqual(terkirim, ['pengingat pagi']);
mock.timers.tick(9 * 60 * 60 * 1000);
assert.deepStrictEqual(terkirim, ['pengingat pagi', 'laporan sore']);
mock.timers.reset();
});
test('jam palsu membekukan waktu', () => {
mock.timers.enable({ apis: ['Date'] });
mock.timers.setTime(new Date(2026, 0, 15, 8, 0, 0).getTime());
assert.strictEqual(new Date().getHours(), 8);
assert.strictEqual(new Date().getDate(), 15);
mock.timers.reset();
});node --test timer-palsu.test.mjsOutput yang Dihasilkan:
✔ menunggu pesan terjadwal tanpa menunggu sungguhan
✔ jam palsu membekukan waktu
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0Test pertama menjalankan logika penjadwalan delapan jam tanpa menunggu satu milidetik pun sungguhan. Urutan kerjanya:
- Baris 6:
mock.timers.enable({ apis: ['setTimeout'] })menggantisetTimeoutglobal dengan versi palsu; hanya API yang disebut yang diganti, sisanya tetap asli. - Baris 8-9: dua pesan dijadwalkan pada jam ke-8 dan ke-17 — dengan timer palsu, penjadwalan ini hanya dicatat, tidak menunggu.
- Baris 11-15:
tick()memajukan waktu palsu; setiaptickmenjalankan semua callback yang batas waktunya terlampaui, sehingga kita bisa memeriksa isiterkirimpada titik-titik waktu tertentu. - Baris 21-28 (jam palsu):
enable({ apis: ['Date'] })mengganti classDate;setTime()membekukan waktu pada 15 Januari 2026 pukul 08:00, lalunew Date()membaca jam yang beku itu. - Baris 29 dan 16:
mock.timers.reset()mengembalikan semua timer dan jam ke perilaku asli — wajib dipanggil agar test berikutnya tidak mewarisi dunia palsu.
Batas Waktu Test #
Test yang menggantung — misalnya menunggu respons yang tidak pernah datang — akan menahan seluruh suite selamanya. Opsi timeout pada deklarasi test memberi batas: melewatinya membatalkan test dan menandai berkas gagal. Berkas berikut mensimulasikan layanan yang lambat dengan jeda 1500 milidetik sementara batasnya hanya 100 milidetik:
import { test } from 'node:test';
test('layanan lambat harus selesai di bawah 100 milidetik', { timeout: 100 }, async () => {
await new Promise((resolve) => setTimeout(resolve, 1500));
});node --test layanan-lambat.test.mjs
echo "kode keluar: $?"Output yang Dihasilkan:
✖ layanan lambat harus selesai di bawah 100 milidetik
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 0
ℹ cancelled 1
ℹ skipped 0
ℹ todo 0
✖ failing tests:
test at layanan-lambat.test.mjs:3:1
✖ layanan lambat harus selesai di bawah 100 milidetik
'test timed out after 100ms'
kode keluar: 1Test tersebut dilaporkan sebagai cancelled — bukan fail — dengan pesan test timed out after 100ms, dan kode keluar tetap 1 sehingga pipeline CI berhenti. Tanpa opsi timeout, batas bawaannya tidak terbatas: test yang menggantung akan menggantung pula seluruh proses node --test. Karena itu biasakan memasang timeout pada test yang menyentuh jaringan atau proses eksternal, sambil tetap menjaga jeda yang wajar — batas 100 milidetik pada contoh di atas memang sengaja ketat untuk memicu pembatalan.
Ke Mana Setelah Ini #
Kita kini dapat menguji kode asynchronous dengan aman: menunggu promise lewat async/await, menangkap penolakan dengan assert.rejects, membungkus callback menjadi promise, mempercepat timer, dan memasang batas waktu. Satu rintangan tersisa pada kode nyata: dependensi eksternal — jaringan, berkas, layanan pihak ketiga — yang membuat test lambat atau tidak menentu. Chapter Mocking mengajarkan cara menggantikan dependensi tersebut dengan panggung palsu dari node:test.