Boundary (batas) adalah garis tempat kodemu bertemu kode yang gak kamu tulis: library, API bawaan browser, SDK payment gateway. Learning test adalah test kecil yang kamu tulis untuk mengunci cara kode orang lain itu bekerja, sebelum kamu bergantung padanya. Istilahnya dari bab 8, "Boundaries", buku Clean Code (Robert C. Martin, 2008); bab itu ditulis James Grenning, yang mengambil nama learning tests dari Jim Newkirk. Bayangin Raka membeli printer struk baru. Sebelum dipakai di kasir, ia mencetak satu struk percobaan berisi Rp 220.500 untuk memastikan titik ribuannya keluar dan tulisan Rp-nya muat. Struk itu disimpan di laci. Tahun depan printernya diganti merek lain, struk yang sama dicetak lagi dan dibandingkan. Struk percobaan itu learning test-nya.
🧪 Tebakan cepat:
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(220500) === 'Rp 220.500'bernilaitrueataufalse? Tebak dulu, jawabannya ada di bagian "Learning test: tulis asumsimu".
Batas: kode yang bukan milikmu
formatRupiah di kasir Raka cuma satu baris, 'Rp ' + amount.toLocaleString('id-ID'). Tapi yang memutuskan 220500 jadi 220.500 bukan kodemu: itu Intl, bagian dari engine JavaScript browser, dengan data bahasa dari CLDR (Common Locale Data Repository). Begitu juga new Date(teks) yang membaca tanggal, dan SDK payment gateway yang menagih pembeli. Kodemu berisi asumsi tentang kode orang lain, dan asumsi itu belum pernah dicek.
Learning test: tulis asumsimu
Tulis tiap asumsi sebagai satu pemeriksaan kecil yang memanggil library aslinya:
function show(value) {
return JSON.stringify(value).replace(/\u00a0/g, '\\u00a0');
}
function check(actual, expected, label) {
if (actual === expected) console.log('OK ' + label);
else console.log('BEDA ' + label + ': ' + show(actual) + ', harusnya ' + show(expected));
}
check((220500).toLocaleString('id-ID'), '220.500', 'pemisah ribuan id-ID');
check(
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(220500),
'Rp 220.500',
'currency IDR',
);
check(new Date('12/03/2026').getMonth(), 2, '12/03/2026 bulan Maret');
checkmencetakOKatauBEDAdengan nilai aslinya, seperti cek keranjang di lesson-lesson sebelumnya.showmenulis spasi khusus (no-break space, kode 160) sebagai\u00a0, supaya yang gak kelihatan jadi kelihatan.
Hasilnya di Chrome dan Node saat lesson ini ditulis:
OK pemisah ribuan id-ID
BEDA currency IDR: "Rp\u00a0220.500", harusnya "Rp 220.500"
BEDA 12/03/2026 bulan Maret: 11, harusnya 2
Jawabannya false. Itu jawaban tebakannya. Di Chrome, spasi setelah Rp adalah no-break space, bukan spasi biasa, jadi teks yang di layar terlihat sama tetap gak ===. Di Safari hasilnya lain lagi: Rp220.500,00, tanpa spasi dan dengan desimal. Kode yang sama, dua browser, dua jawaban. Satu test kecil mengajarkan keputusannya: kasir tetap memakai toLocaleString('id-ID'), yang hasilnya 220.500 di semua browser, dan test pertama tadi mengunci itu.
Tanggal 12/03/2026
Orang Indonesia membaca 12/03/2026 sebagai 12 Maret. new Date('12/03/2026').getMonth() mengembalikan 11 (bulan dihitung dari 0, jadi 11 = Desember): browser membacanya bulan/tanggal gaya Amerika, 3 Desember. Spesifikasi JavaScript cuma menjamin format ISO seperti 2026-03-12; format lain seperti 12/03/2026 terserah engine. Jadi kasir membaca tanggal sendiri:
function parseTanggal(text) {
const [day, month, year] = text.split('/').map(Number);
return new Date(year, month - 1, day);
}
check(parseTanggal('12/03/2026').getMonth(), 2, 'parseTanggal: Maret'); // OK
Learning test yang gagal tadi diubah jadi catatan kenyataan, check(new Date('12/03/2026').getMonth(), 11, '12/03/2026 dibaca bulan/tanggal'), supaya siapa pun yang bertanya "kenapa gak pakai new Date saja?" langsung menemukan jawabannya.
Saat library berubah
Setelah asumsi diperbaiki, learning test berisi perilaku yang sebenarnya, dan ikut dijalankan di CI tiap kali browser, Node atau library di-upgrade. Kalau perilakunya berubah, test ini merah lebih dulu, sebelum pembeli menemukan struk yang aneh. Ini pernah terjadi sungguhan: data CLDR 42 (akhir 2022) mengganti spasi sebelum AM/PM di format jam bahasa Inggris dengan spasi sempit U+202F, dan banyak test yang membandingkan teks jam gagal setelah upgrade.
Di Lab, stripe "versi 2" (rekaan) menghapus toFixed(2) dan Math.round. Learning test yang dulu OK langsung bicara:
BEDA format 50 padding: "$0.5", harusnya "$0.50"
BEDA parse $1.15: 114.99999999999999, harusnya 115
parseFloat('1.15') * 100 memang 114.99999999999999 di JavaScript (angka desimal gak tersimpan persis); Math.round di versi 1 yang menutupi itu.
Bungkus di satu tempat
Learning test paling berguna kalau library-nya cuma dipanggil di satu tempat milikmu, sebuah adapter. formatRupiah dan parseTanggal sudah adapter: struk, email dan laporan memanggil mereka, bukan Intl atau Date. Untuk pembayaran juga begitu. Kalau SDK vendor dipanggil langsung di 50 file, pindah dari Stripe ke Midtrans atau Xendit berarti mengedit 50 file. Dengan adapter yang bicara bahasa toko, bukan bahasa vendor:
// paymentGateway.js: satu-satunya file yang kenal SDK vendor
const paymentGateway = {
async charge(order) {
const result = await vendorSdk.createCharge({ amount: order.total, currency: 'IDR' });
return { id: result.id, paid: result.status === 'succeeded' };
},
async refund(paymentId) {
await vendorSdk.createRefund({ charge: paymentId });
},
};
Nama method SDK di atas cuma contoh; tiap vendor punya namanya sendiri, dan justru itu yang disembunyikan. Checkout cukup memanggil paymentGateway.charge(order). Ganti vendor, yang ditulis ulang cuma file ini, dan learning test-nya menunjukkan apa yang berbeda di vendor baru.
Kapan dibungkus, kapan langsung
- Bungkus library yang bisa diganti (Stripe untuk pembayaran, SendGrid untuk email, AWS SDK untuk layanan cloud), yang dipakai di banyak tempat, yang hasilnya berbeda antar versi atau browser (format angka, tanggal), atau yang ada di jalur kritis (pembayaran, login).
- Pakai langsung framework yang jadi fondasi seluruh aplikasi, seperti React: menggantinya berarti menulis ulang UI, dan adapter cuma menambah lapisan tanpa manfaat. Begitu juga API standar yang stabil, seperti
Array.prototype.mapatauMath.round. - Learning test memanggil library asli. Tiruan (mock) yang selalu mengembalikan
'Rp 220.500'cuma menguji tiruanmu sendiri, dan tetap hijau saat browser berubah.
Latihan: learningTests untuk stub stripe
stripe di latihan adalah stub: tiruan kecil library pembayaran yang menghitung dalam sen (satuan terkecil) dan menulis dolar. stripe.format(cents) mengubah 1234 jadi "$12.34" dan 50 jadi "$0.50"; stripe.parse(str) kebalikannya, "$12.34" jadi 1234. Helper assert(actual, expected, label) sudah ada dan mengembalikan { label, pass, actual, expected }. Di learningTests(), push empat hasil assert ke results, dengan urutan dan label persis:
results.push(assert(stripe.format(1234), '$12.34', 'format 1234'));
results.push(assert(stripe.format(50), '$0.50', 'format 50 padding'));
results.push(assert(stripe.parse('$12.34'), 1234, 'parse $12.34'));
results.push(assert(stripe.format(stripe.parse('$7.89')), '$7.89', 'round-trip $7.89'));
Enam console.log di bawahnya mencetak 4, true, format 1234, $0.50, 1234, round-trip $7.89. Pemeriksaan bolak-balik (round-trip), dari teks ke angka lalu kembali ke teks, ada karena di situlah masalah pembulatan biasanya muncul. Stub-nya jangan diubah: pemeriksa juga memanggil stripe.format(305), stripe.parse("$0.07") dan assert(...) dengan angka lain.
⚠️ Jebakan yang sering ketemu
- Learning test dengan mock. Yang diuji jadi asumsimu sendiri, bukan library-nya; panggil yang asli.
- Membandingkan teks hasil format dengan spasi biasa.
Rp 220.500dariIntlbisa berisi no-break space, dan tiap browser bisa berbeda. Cek kode karakternya, atau jangan bergantung pada format itu. - Library dipanggil langsung di banyak file. Tiap perubahan perilaku harus dicari di semua tempat; bungkus di satu adapter dengan bahasa domainmu.
- Membungkus semuanya. React atau
Array.mapgak perlu adapter. - Di latihan, label atau urutan beda sedikit.
'format 50 padding'dan'round-trip $7.89'harus persis, dan urutan push menentukanresults[0]sampairesults[3].
TL;DR: Boundary adalah batas antara kodemu dan kode yang gak kamu tulis (library, Intl, Date, SDK pembayaran); learning test (Clean Code bab 8, istilah Jim Newkirk) adalah test kecil yang memanggil library asli untuk mengecek asumsimu. Intl.NumberFormat gaya currency IDR untuk 220500 bukan 'Rp 220.500': Chrome dan Node memakai no-break space (kode 160), Safari menulis Rp220.500,00; toLocaleString('id-ID') yang menghasilkan 220.500 di semua browser, jadi kasir tetap memakainya. new Date('12/03/2026') dibaca 3 Desember (getMonth() 11), jadi tanggal dibaca dengan parseTanggal. Setelah dikoreksi, learning test mengunci perilaku sebenarnya dan dijalankan tiap upgrade, jadi perubahan library (seperti spasi U+202F di CLDR 42) ketahuan sebelum production. Bungkus library yang bisa diganti atau berubah di satu adapter yang bicara bahasa domain (formatRupiah, paymentGateway.charge, refund); framework fondasi seperti React dan API standar dipakai langsung. Di latihan: empat assert di-push ke results dengan label format 1234, format 50 padding, parse $12.34, round-trip $7.89, dan output 4, true, format 1234, $0.50, 1234, round-trip $7.89.