Pengujian otomatis memeriksa kode dengan kode, sehingga perilaku sebuah fungsi dapat diverifikasi ulang berkali-kali tanpa mengetik masukan yang sama secara manual. Topik ini membedah unit test (unit test), yaitu pengujian untuk unit terkecil program seperti satu fungsi, mulai dari pola susun-eksekusi-periksa sampai test runner, program yang menjalankan sekumpulan kasus uji lalu melaporkan hasilnya per kasus. Runner mininya dibangun sendiri dari modul node:assert agar mekanismenya terlihat, dan di bagian akhir bentuk yang sama ditulis ulang dengan Vitest sebagai perkakas produksinya.
Mengapa pengujian otomatis#
Memeriksa fungsi dengan mengetikkan masukannya di konsol memang selesai dalam sekejap, tetapi pemeriksaan yang sama harus diulang setiap kali kode disunting, dan jumlah pemeriksaan bertambah seiring bertambahnya fitur. Pembaruan yang tampak kecil bisa merusak perilaku lama tanpa terasa, dan kondisi seperti itu disebut regresi (regression). Kumpulan kasus uji yang dapat dijalankan dengan satu perintah menjaga perilaku lama tetap terpantau, karena setiap perubahan langsung diuji ulang terhadap semua kasus yang sudah ada.
Kasus uji juga mendokumentasikan maksud kode dengan cara yang tidak bisa dibantah. Fungsi penghitung potongan harga yang disertai kasus “belanja tepat 100000 mendapat potongan 10 persen” menjelaskan aturan batasnya lebih jelas daripada komentar apa pun. Bila aturannya memang berubah, kasus ujinya ikut disunting sehingga perbedaannya terlihat pada hasil pengujian. Karena itulah proyek profesional hampir selalu mengunci lapisan logikanya dengan unit test sebelum kode tersebut dipakai lebih luas. Manfaat kebiasaan ini paling terasa pada masa refactor, saat struktur kode berubah tetapi perilakunya tidak boleh.
Anatomi satu kasus uji#
Satu kasus uji mengikuti pola tiga langkah yang lazim disebut susun-eksekusi-periksa (arrange-act-assert). Tahap susun menyiapkan masukan dan nilai yang diharapkan, tahap eksekusi memanggil fungsi yang diuji tepat satu kali, dan tahap periksa membandingkan hasil dengan harapan. Alat periksanya berupa asersi (assertion), fungsi yang diam bila perbandingannya benar dan melempar error bila salah.
// tidak-dijalankan
// susun: siapkan masukan dan hasil yang diharapkan
const total = 100000;
const diharapkan = 10000;
// eksekusi: panggil fungsi yang diuji satu kali
const hasil = hitungPotongan(total);
// periksa: bandingkan hasil dengan harapan
assert.equal(hasil, diharapkan);Modul node:assert menyediakan asersi bawaan Node.js tanpa instalasi apa pun, dan assert.equal membandingkan dua nilai dengan ketat ketika diimpor dari node:assert/strict. Bila asersinya lolos, tidak ada keluaran sama sekali, sedangkan bila gagal, asersinya melempar error AssertionError yang memuat selisih kedua nilai tersebut. Ketiadaan keluaran menandai kasus yang lulus, sedangkan error dari asersi menandai kasus yang gagal. Itulah sinyal yang dimanfaatkan runner untuk melaporkan hasil per kasus.
Runner mini dari node:assert#
Runner mini berikut menguji fungsi hitungPotongan, penghitung potongan harga untuk daftar belanja dengan dua tingkat potongan. Berkasnya memuat dua versi fungsi sekaligus, versi lama yang diambil dari kode sebelum disunting dan versi perbaikannya, sehingga satu eksekusi memperlihatkan hasil merah dan hijau berdampingan. Kedua versi diuji dengan daftar kasus yang sama melalui fungsi jalankanSatuSet, dan setiap kasus dilaporkan apa adanya.
// Runner mini: menjalankan setiap kasus uji dan melaporkan hasilnya per kasus.
import assert from "node:assert/strict";
// Versi lama: batas kualifikasi memakai > sehingga kasus tepat 100000 terlewat.
function hitungPotonganLama(total) {
if (total >= 250000) return (total * 20) / 100;
if (total > 100000) return (total * 10) / 100;
return 0;
}
// Versi perbaikan: batas pertama dan kedua sama-sama memakai >=.
function hitungPotongan(total) {
if (total >= 250000) return (total * 20) / 100;
if (total >= 100000) return (total * 10) / 100;
return 0;
}
const kasus = [
{ nama: "belanja 50000 tanpa potongan", total: 50000, diharapkan: 0 },
{ nama: "belanja 150000 mendapat potongan 10 persen", total: 150000, diharapkan: 15000 },
{ nama: "belanja tepat 100000 mendapat potongan 10 persen", total: 100000, diharapkan: 10000 },
{ nama: "belanja 300000 mendapat potongan 20 persen", total: 300000, diharapkan: 60000 },
];
function jalankanSatuSet(judul, fungsi) {
console.log(judul);
let lulus = 0;
for (const k of kasus) {
const hasil = fungsi(k.total);
try {
assert.equal(hasil, k.diharapkan);
lulus += 1;
console.log(`LULUS ${k.nama}`);
} catch {
console.log(`GAGAL ${k.nama}: diharapkan ${k.diharapkan}, diterima ${hasil}`);
}
}
console.log(`ringkasan: ${lulus} lulus, ${kasus.length - lulus} gagal`);
}
jalankanSatuSet("versi lama:", hitungPotonganLama);
jalankanSatuSet("versi perbaikan:", hitungPotongan);versi lama:
LULUS belanja 50000 tanpa potongan
LULUS belanja 150000 mendapat potongan 10 persen
GAGAL belanja tepat 100000 mendapat potongan 10 persen: diharapkan 10000, diterima 0
LULUS belanja 300000 mendapat potongan 20 persen
ringkasan: 3 lulus, 1 gagal
versi perbaikan:
LULUS belanja 50000 tanpa potongan
LULUS belanja 150000 mendapat potongan 10 persen
LULUS belanja tepat 100000 mendapat potongan 10 persen
LULUS belanja 300000 mendapat potongan 20 persen
ringkasan: 4 lulus, 0 gagalKeluarannya berbentuk laporan kasus per kasus. Versi lama melewati tiga kasus dan gagal tepat pada belanja 100000, karena kondisi total > 100000 melewatkan angka yang sama dengan batasnya sendiri, sedangkan versi perbaikan yang mengganti operatornya menjadi >= lulus pada seluruh kasus. Baris ringkasan di akhir setiap set memudahkan pembacaan sekilas, dan inilah wujud paling sederhana dari keluaran test runner.
Kasus yang gagal itu bukan kebetulan, kasusnya didesain untuk menangkap bug batas tersebut. Menulis kasus uji lebih dahulu sebelum perbaikan dibuat adalah inti pengembangan berbasis uji (test-driven development), yaitu merah terlebih dahulu, hijau kemudian. Kasus merah membuktikan bug ada dan terukur, kasus hijau membuktikan perbaikannya benar, dan keduanya kini menjadi jaring pengaman permanen untuk setiap suntingan berikutnya pada fungsi tersebut.
Dari runner mini ke Vitest#
Runner mini sengaja dibangun sendiri supaya mekanismenya terlihat, yaitu perulangan atas kasus, pemanggilan asersi, dan penangkapan error dengan try-catch. Untuk pekerjaan harian, runner itu digantikan test runner produksi seperti Vitest atau Jest. Keduanya menyediakan fasilitas yang tidak praktis dibuat sendiri, misalnya penemuan berkas uji otomatis, mode pengawasan (watch mode) yang menjalankan ulang pengujian setiap berkas disimpan, eksekusi paralel, dan laporan lengkap. API-nya dirancang serupa dengan pola yang sudah dipraktikkan, sehingga perpindahan dari runner mini ke sintaksnya tidak menuntut cara pikir baru.
// tidak-dijalankan
import { describe, it, expect } from "vitest";
import { hitungPotongan } from "./diskon.mjs";
describe("hitungPotongan", () => {
it("memberi potongan 10 persen pada belanja tepat 100000", () => {
expect(hitungPotongan(100000)).toBe(10000);
});
});Fungsi describe mengelompokkan kasus uji menjadi satu kelompok bernama, it membungkus satu kasus lengkap dengan kalimat pemeriksaannya, dan expect menggantikan asersi manual dengan pencocok seperti toBe. Kasus pada contoh tersebut sama persis dengan kasus merah-hijau pada runner mini, hanya dibungkus sintaks Vitest. Runner-nya kemudian dipasang sebagai dependency pengembangan dan dipanggil lewat npm script agar perintahnya seragam di seluruh proyek.
{
"scripts": {
"test": "vitest"
}
}# tidak-dijalankan
# memasang Vitest sebagai dependency pengembangan, cukup sekali per proyek
npm install --save-dev vitest
# menjalankan seluruh kasus uji lewat npm script
npm run testRingkasan#
- Unit test menguji unit terkecil program dengan pola susun-eksekusi-periksa, dan asersi menjadi titik periksanya.
- Runner mini dari
node:assert/strictmemperlihatkan mekanisme test runner, yaitu perulangan kasus, asersi, dan penangkapan error per kasus. - Kasus uji yang ditulis lebih dahulu menangkap bug sebelum perbaikan dibuat, dan kasus yang sama membuktikan perbaikannya.
- Vitest menyediakan mekanisme yang sama dengan kenyamanan produksi, dari penemuan berkas uji otomatis sampai eksekusi paralel.
Referensi#
- Getting Started — Vitest — panduan resmi memasang dan menjalankan Vitest pada proyek.
- expect API — Vitest
— daftar pencocok
expectsepertitoBedantoEqual. - assert — Node.js — dokumentasi modul asersi bawaan yang dipakai runner mini.