Test runner bawaan node:test telah membawa kita dari assertion pertama sampai integration test tanpa satu pun dependency. Namun proyek JavaScript di luar sana juga hidup di atas runner lain, dan yang paling banyak dipakai belakangan ini adalah Vitest — framework testing yang dibangun di atas Vite dengan API yang kompatibel dengan Jest. Kabar baiknya, hampir semua kebiasaan dari Section ini terbawa langsung: pola Arrange, Act, Assert, nama test yang menjelaskan perilaku, dan isolasi antar test. Yang berubah terutama peralatannya: assertion bergaya expect(...).toBe(...) yang berantai, watch mode yang cepat, dan laporan kegagalan yang kaya detail. Pada chapter penutup Section ini kita mencicipi Vitest — menulis test pertama, membandingkan matchernya dengan node:assert, lalu menjalankannya dari terminal dengan berbagai reporter.
Test Pertama dengan Vitest #
Vitest sudah tersedia di lingkungan buku ini sehingga cukup dipanggil lewat npx; pada proyek kita sendiri, cara resminya adalah memasang sebagai devDependency dengan npm install -D vitest seperti pada panduan memulai Vitest
. Berkas test pertama kita sengaja dibuat seminimal mungkin:
import { test, expect } from 'vitest';
function jumlah(a, b) { return a + b; }
test('menjumlahkan dua angka', () => {
expect(jumlah(2, 3)).toBe(5);
});
test('menangani angka negatif', () => {
expect(jumlah(-1, 1)).toBe(0);
});Penamaan jumlah.vitest.test.mjs memuat dua penanda sekaligus: akhiran .test.mjs membuat berkas dikenali sebagai berkas test, sedangkan sisipan vitest di tengah menegaskan pemiliknya — ingat bahwa node --test juga mengenali pola *.test.mjs, jadi penanda ini menjaga kedua runner tidak saling merebut berkas yang sama. Mari kita bedah isinya:
- Baris 1:
testdanexpectdiimpor dari packagevitest— tanpa prefixnode:karena ini package eksternal, bukan module bawaan. - Baris 3: untuk contoh pertama, function yang diuji didefinisikan di berkas yang sama; pada contoh berikutnya kita kembali memisahkannya ke module sendiri.
- Baris 5-7:
test(nama, callback)— bentuk yang sama persis dengannode:test, sehingga cara menyusun suite tidak perlu dipelajari ulang. - Baris 6:
expect(hasil).toBe(harapan)— inilah wajah khas Vitest: nilai nyata dibungkusexpect(), lalu matcher.toBe()membandingkannya secara ketat sepertiassert.strictEqual.
Saatnya menjalankannya. Subperintah run berarti sekali jalan tanpa watch mode:
npx vitest run jumlah.vitest.test.mjsOutput yang Dihasilkan:
RUN v5.0.5
✓ jumlah.vitest.test.mjs (2 tests)
Test Files 1 passed (1)
Tests 2 passed (2)Matcher expect untuk Object dan Error #
Untuk matcher yang lebih beragam, kita kembali ke pola yang sehat: kode yang diuji dipisah ke module sendiri. Module berikut membuat object pengguna dan menolak nama kosong:
export function buatPengguna(nama, peran) {
if (!nama) {
throw new Error('nama wajib diisi');
}
return { nama, peran };
}Perhatikan perilaku baris 5: karena peran tidak wajib, pemanggil yang tidak mengirim argumen kedua membuat properti peran bernilai undefined — detail ini akan menjadi bahan uji yang menarik sebentar lagi. Berkas testnya:
import { describe, it, expect } from 'vitest';
import { buatPengguna } from './pengguna.mjs';
describe('buatPengguna', () => {
it('membuat pengguna dengan nama dan peran', () => {
expect(buatPengguna('Budi', 'admin')).toEqual({ nama: 'Budi', peran: 'admin' });
});
it('nama pengguna berupa teks', () => {
expect(buatPengguna('Budi', 'admin').nama).toBe('Budi');
});
it('menolak nama kosong dengan error', () => {
expect(() => buatPengguna('')).toThrow('nama wajib diisi');
});
});Tiga matcher berbeda bekerja di dalamnya:
- Baris 6 (
toEqual): perbandingan mendalam — seluruh properti object dibandingkan berulang sampai daun. Pasangannya dinode:assertadalahassert.deepStrictEqual. - Baris 10 (
toBe): perbandingan ketat untuk nilai primitif; untuk object, matcher ini memeriksa identitas referensi yang sama. - Baris 14 (
toThrow): function dibungkus arrow tanpa dipanggil, lalu matcher memeriksa bahwa pemanggilannya melempar error; cukup memberi potongan pesan ('nama wajib diisi') karena pencocokannya terhadaperror.message.
npx vitest run pengguna.vitest.test.mjsOutput yang Dihasilkan:
RUN v5.0.5
✓ pengguna.vitest.test.mjs (3 tests)
Test Files 1 passed (1)
Tests 3 passed (3)Bagi kita yang datang dari node:test, pemetaan berikut merangkum perjalanan sejauh ini:
| Pada node:test | Padanan di Vitest |
|---|---|
test(), describe(), it() | Sama — nama dan bentuknya identik |
beforeEach(), afterEach() | Sama — hook bernama sama, termasuk before dan after |
assert.strictEqual(x, 5) | expect(x).toBe(5) |
assert.deepStrictEqual(a, b) | expect(a).toStrictEqual(b) |
assert.throws(fn, { message }) | expect(fn).toThrow('pesan') |
Daftar matcher lengkapnya jauh lebih panjang — toContain, toHaveLength, toBeGreaterThan, sampai toMatchSnapshot — dan semuanya terdokumentasi pada referensi API expect
.
toEqual versus toStrictEqual #
Dua matcher mendalam di Vitest tidak setara, dan bedanya persis di titik peran yang undefined tadi. toEqual mengabaikan properti bernilai undefined selama perbandingan; toStrictEqual menolaknya karena struktur object dianggap berbeda. Berkas berikut menguji keduanya bersebelahan — dan test keduanya sengaja kita tulis untuk melihat kegagalannya:
import { it, expect } from 'vitest';
import { buatPengguna } from './pengguna.mjs';
it('toEqual menerima peran yang undefined', () => {
expect(buatPengguna('Siti')).toEqual({ nama: 'Siti' });
});
it('toStrictEqual menolak peran yang undefined', () => {
expect(buatPengguna('Siti')).toStrictEqual({ nama: 'Siti' });
});Test pertama lulus: { nama: 'Siti', peran: undefined } dianggap cukup sama dengan { nama: 'Siti' } oleh toEqual. Test kedua gagal — dan justru di sinilah Vitest bersinar. Jalankan berkasnya:
npx vitest run ketat.vitest.test.mjs
echo "kode keluar: $?"Output yang Dihasilkan:
RUN v5.0.5
❯ ketat.vitest.test.mjs (2 tests | 1 failed)
✓ toEqual menerima peran yang undefined
× toStrictEqual menolak peran yang undefined
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL ketat.vitest.test.mjs > toStrictEqual menolak peran yang undefined
AssertionError: expected { nama: 'Siti', peran: undefined } to strictly equal { nama: 'Siti' }
- Expected
+ Received
{
"nama": "Siti",
+ "peran": undefined,
}
❯ ketat.vitest.test.mjs:9:32
7|
8| it('toStrictEqual menolak peran yang undefined', () => {
9| expect(buatPengguna('Siti')).toStrictEqual({ nama: 'Siti' });
| ^
10| });
11|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
kode keluar: 1Mari kita baca laporan kegagalannya bagian demi bagian:
- Baris
FAIL: nama berkas dan nama test yang gagal — setara dengan blok failing tests padanode --test, tetapi hadir di bagian atas laporan. AssertionError: expected { nama: 'Siti', peran: undefined } to strictly equal { nama: 'Siti' }: pesan kegagalan menyebut kedua nilai secara eksplisit, bukan sekadar “nilai tidak sama”.- Blok
- Expected/+ Received: perbandingan ala diff — tanda-menandai nilai yang kita harapkan, tanda+nilai yang benar-benar diterima. Baris+ "peran": undefinedlangsung menunjuk properti penyebabnya. - Baris
❯ ketat.vitest.test.mjs:9:32beserta cuplikan kode: lokasi persis assertion yang gagal lengkap dengan baris kode di sekitarnya — koordinatbaris:kolomsama seperti yang kita kenal dari membaca stack trace. - Rangkuman
Tests 1 failed | 1 passed (2)dankode keluar: 1: satu dari dua test gagal sehingga proses keluar dengan kode 1 — pipeline CI akan menahan penggabungan, sama seperti perilakunode --test.
Reporter yang Bisa Dipilih #
Reporter bawaan menyembunyikan nama test yang lulus — baris ✓ pengguna.vitest.test.mjs (3 tests) hanya merangkum. Reporter verbose menampilkan setiap test sebaris, berguna saat kita ingin memastikan seluruh nama test terbaca seperti spesifikasi:
npx vitest run --reporter=verbose pengguna.vitest.test.mjsOutput yang Dihasilkan:
RUN v5.0.5
✓ pengguna.vitest.test.mjs > buatPengguna > membuat pengguna dengan nama dan peran
✓ pengguna.vitest.test.mjs > buatPengguna > nama pengguna berupa teks
✓ pengguna.vitest.test.mjs > buatPengguna > menolak nama kosong dengan error
Test Files 1 passed (1)
Tests 3 passed (3)Nama lengkap setiap test kini tampak — termasuk nama suite buatPengguna sebagai awalan. Reporter lain membidik kebutuhan yang berbeda: dot memadatkan hasil jadi titik-titik untuk suite raksasa, junit menghasilkan XML yang dibaca alat CI, dan tap menghasilkan format Test Anything Protocol yang mudah diurai mesin:
npx vitest run --reporter=tap pengguna.vitest.test.mjsOutput yang Dihasilkan:
TAP version 13
1..1
ok 1 - pengguna.vitest.test.mjs {
1..1
ok 1 - buatPengguna {
1..3
ok 1 - membuat pengguna dengan nama dan peran
ok 2 - nama pengguna berupa teks
ok 3 - menolak nama kosong dengan error
}
}Setiap baris ok memuat satu test; angka 1..3 menyatakan rentang rencana, dan sarang kurung kurawal menggambarkan struktur suite. Angka durasi per test (# time=...) aslinya tercetak — perintah di buku menyaringnya dengan alasan yang sama seperti sebelumnya. Daftar lengkap beserta cara menggabungkan beberapa reporter sekaligus ada pada dokumentasi CLI Vitest
.
Mode Watch untuk Siklus Cepat #
Sepanjang chapter ini kita memakai vitest run karena buku butuh eksekusi sekali jalan. Saat mengembangkan, bentuk yang lebih nyaman justru tanpa run: npx vitest pada terminal interaktif masuk ke watch mode — runner menunggu, lalu menjalankan ulang test setiap kali berkas berubah, dan hanya test yang terdampak perubahan itulah yang dijalankan ulang. Di lingkungan tanpa terminal interaktif seperti pipeline CI, Vitest otomatis jatuh ke perilaku run sehingga perintah yang sama aman dipakai di kedua tempat.
Ke Mana Setelah Ini #
Section Testing resmi tuntas: kita bisa menulis test dengan node:test, menguji kode asynchronous, memalsukan dependensi lewat mocking, mengukur coverage, menyusun integration test untuk HTTP server, dan kini mengenal Vitest sebagai alternatif dari ekosistem. Namun test hanya memberi tahu ada yang salah — mencari tahu mengapa salah adalah pekerjaan lain. Section berikutnya membekali kita untuk itu, dimulai dari mengintip ke dalam proses yang sedang berjalan pada Chapter Debugging dengan node –inspect.