Ketika kamu memakai library atau API pihak ketiga — axios, Stripe, AWS SDK, Redis — kamu menyerahkan sebagian kontrol ke kode orang lain. Uncle Bob menyebut ini boundary: batas antara kodemu dan dunia luar.
Boundary yang tidak dikelola dengan baik = sumber bug yang menyakitkan. Library update → sintaks berubah → setengah aplikasimu error. Vendor ganti API → kamu terjebak rewrite massal.
Bagian 1: Learning Test
Cara belajar library baru yang aman: tulis test dulu untuk memverifikasi asumsimu tentang bagaimana library berperilaku.
// Learning test untuk axios
import axios from "axios";
test("axios returns data property, not the raw JSON", async () => {
const res = await axios.get("https://api.example.com/users/1");
// Asumsiku: res adalah objek dengan .data, bukan raw JSON
expect(res).toHaveProperty("data");
expect(res.data).toEqual(expect.objectContaining({ id: 1 }));
});
test("axios throws on 4xx by default", async () => {
await expect(
axios.get("https://api.example.com/does-not-exist")
).rejects.toThrow();
});
Mengapa ini berharga?
- Dokumentasi hidup — tim baru baca test ini dan paham cara pakai library.
- Regression detector — saat update versi library, jalankan test ini. Jika gagal, perilaku library berubah.
- Belajar terstruktur — daripada trial-and-error di
index.js, kamu bangun pemahaman sistematis.
Kapan tulis learning test?
- Saat memakai library baru pertama kali
- Saat akan upgrade mayor version
- Saat menemukan perilaku library yang tidak sesuai ekspektasi (reproduksi bug)
Bagian 2: Adapter Pattern untuk Boundary
Jangan panggil library pihak ketiga langsung dari seluruh kodemu. Bungkus dengan adapter.
// BURUK: axios menyebar di 50 file
// userService.js
import axios from "axios";
const res = await axios.get("/users");
// orderService.js
import axios from "axios";
const res = await axios.get("/orders");
// productService.js
import axios from "axios";
const res = await axios.get("/products");
// Kalau mau ganti axios → fetch native, edit 50 file!
// BERSIH: adapter tunggal
// lib/httpClient.js
import axios from "axios";
export const httpClient = {
get: async (url) => {
const res = await axios.get(url);
return res.data; // sudah unwrap, konsumen tidak perlu tahu
},
post: async (url, body) => {
const res = await axios.post(url, body);
return res.data;
},
};
// userService.js
import { httpClient } from "./lib/httpClient";
const users = await httpClient.get("/users");
Sekarang ganti axios → fetch hanya edit satu file: httpClient.js.
Bagian 3: Stripe / SDK besar — lebih penting lagi
// BURUK: seluruh aplikasi kenal Stripe SDK
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_KEY);
// checkoutService.js
const customer = await stripe.customers.create({ email, source });
const charge = await stripe.charges.create({ customer: customer.id, amount, currency });
// subscriptionService.js
const sub = await stripe.subscriptions.create({ customer, items });
// Kalau kita ganti ke Midtrans (provider lokal)? Rewrite massal.
// BERSIH: PaymentGateway adapter
// services/PaymentGateway.js
class StripeGateway {
async charge({ email, amount, currency, source }) {
const customer = await stripe.customers.create({ email, source });
const ch = await stripe.charges.create({ customer: customer.id, amount, currency });
return { id: ch.id, status: ch.status };
}
}
// checkoutService.js — tidak kenal Stripe
import { paymentGateway } from "./services/PaymentGateway";
const result = await paymentGateway.charge({ email, amount: 100000, currency: "IDR", source });
// Ganti ke Midtrans? Buat MidtransGateway dengan interface sama.
class MidtransGateway {
async charge({ email, amount, currency, source }) { /* ... */ return { id, status }; }
}
Interface domain, bukan interface vendor:
Adapter kamu harus mengekspos bahasa domain bisnismu (charge, refund, subscribe), bukan bahasa vendor (customers.create, charges.create). Vendor berganti, bahasa domain tetap.
Bagian 4: Kapan NOT pakai adapter?
Adapter bukan selalu tepat. Tanda-tanda kamu butuh adapter:
- Library dipakai di > 3 tempat
- Library adalah vendor berbayar yang bisa diganti
- Kamu takut vendor-lock in
- Library punya API yang tidak cocok dengan gaya codebase
Tanda-tanda adapter tidak perlu:
- Library adalah utility kecil (lodash, date-fns) — switching cost rendah
- Library adalah standar de-facto (React, Express) — ganti = rewrite aplikasi anyway
- Dipakai sekali di satu tempat
Bagian 5: Boundary Tests
Selain learning test saat belajar, tulis boundary test yang memastikan adaptermu benar:
describe("PaymentGateway", () => {
it("returns { id, status } shape for successful charge", async () => {
const result = await paymentGateway.charge({
email: "[email protected]",
amount: 100000,
currency: "IDR",
source: "tok_test",
});
expect(result).toMatchObject({
id: expect.any(String),
status: expect.stringMatching(/succeeded|pending/),
});
});
});
Test ini memakai sandbox Stripe (bukan mock), jadi jika Stripe ubah API, test langsung merah.
Rangkuman:
- Learning test: tulis test untuk memverifikasi asumsi sebelum integrasi production
- Adapter pattern: bungkus library pihak ketiga, ekspos bahasa domainmu
- Boundary test: verifikasi adapter masih berfungsi dengan API vendor saat upgrade
- Judgment call: tidak semua library butuh adapter — timbang switching cost
Kode yang menghormati boundary lebih mudah bertahan saat dunia luar berubah.
🎭 Analogi sehari-hari
Learning tests + boundaries itu kayak stop kontak rumah dengan adapter universal. Listrik di Indonesia 220V, di Jepang 100V, di USA 110V. Tanpa adapter, kabel hardware kena listrik salah voltase = rusak. Kabel di rumahmu kontak ke ADAPTER (boundary), adapter handle convert. Pas pindah negara (ganti library/vendor), cukup ganti ADAPTER, kabel-kabel rumah tetap. Plus learning test = colokin dulu adapter ke voltmeter sebelum kirim listrik ke laptop mahal — verify sebelum risk. Library pihak ketiga sama: bungkus dengan adapter (interface kamu), test perilaku adapter (learning test), pas vendor update / ganti = cukup update adapter, kode aplikasi tetap.
⚠️ Jebakan yang sering ditemui
- Akses library pihak ketiga langsung di banyak file — pas vendor update API, harus ubah 50 file. Pakai adapter.
- Skip learning test — assume library kerja seperti yang kamu bayangkan, ternyata edge case nge-bug. Verify dulu.
- Mock library 100% di test — gak detect breaking change saat library update. Combine: integration test pakai sandbox.
- Wrap SEMUA library — overkill. Library yang stable + standar (lodash) gak perlu adapter.
- Adapter terlalu thin — cuma rename method tanpa abstraksi nilai. Adapter harus EXPOSE bahasa domain kamu, bukan mirror library.
- Boundary test cuma di mock — gak detect real API change. Pakai sandbox untuk critical integrations (payment, auth).
- Semua boundary di-test ekstrim — overkill. Test yang LIKELY berubah (third-party API yang sering update).
Pattern Adapter
// ❌ TANPA adapter — tightly coupled ke axios
import axios from "axios"
const response = await axios.get("/api/users", { headers: {...} })
const users = response.data
// ✓ DENGAN adapter — bahasa domain
class UserApi {
async fetchAll() {
const response = await axios.get("/api/users", { ... })
return response.data.map(u => new User(u))
}
}
const userApi = new UserApi()
const users = await userApi.fetchAll()
// Code aplikasi gak tau axios. Pas pindah ke fetch / ky, cukup ubah UserApi.
Learning Test Example
// Sebelum integrate Stripe SDK ke production, verify behavior
describe("Stripe SDK behavior", () => {
it("creates charge with correct response shape", async () => {
const charge = await stripe.charges.create({
amount: 100000,
currency: "idr",
source: "tok_test",
})
// Asumsi yang kamu cek: response punya field "id" dan "status"
expect(charge).toHaveProperty("id")
expect(charge.status).toMatch(/succeeded|pending|failed/)
})
})
Pas Stripe update API, test ini langsung merah → kamu catch sebelum production.
🎯 Wrap library atau pakai langsung?
- Library tidak stabil / sering breaking change → wrap dengan adapter ✓
- Library di critical path (payment, auth) → wrap + boundary test ✓
- Library kompleks dengan banyak konfigurasi → wrap untuk simplify
- Library yang KEMUNGKINAN diganti → wrap (decouple)
- Library standar industri yang stabil (lodash, date-fns) → pakai langsung
- Library kecil 1-2 method dipake → pakai langsung
- Library yang udah expose bahasa domain kamu → pakai langsung
Aturan: wrap saat ada RISK CHANGE atau COMPLEXITY yang perlu disembunyikan. Sederhana + stable = pakai langsung.
TL;DR: Boundary = batas antara kodemu vs library/vendor pihak ketiga. Learning test = verify perilaku library SEBELUM integrate ke production. Adapter pattern = wrap library, expose bahasa domain kamu — pas vendor update, cukup ubah adapter. Boundary test = verify adapter masih bener pas library update. JANGAN wrap semua library (overkill), wrap yang RISK + COMPLEXITY tinggi (payment, auth, vendor unstable). Pure data utility (lodash, date-fns) — pakai langsung.