Legacy Tidak Berbohong
Kode legacy tidak punya dokumentasi. Kalaupun ada, itu peninggalan tiga tahun lalu. Test-nya tidak ada, atau kalaupun ada, sudah rusak dan ditandai skip. Komentarnya bertentangan dengan kode. Penulis aslinya sudah resign, dan satu-satunya orang yang tahu hanya meninggalkan pesan: “kalau disentuh pasti meledak.”
Namun kode itu sedang berjalan tepat saat ini juga. Ia memproses pembayaran, menerima login, mencatat pesanan.
Dokumentasi berbohong. Komentar pun berbohong. Ingatan manusia berbohong lebih parah lagi. Satu-satunya yang tidak pernah berbohong adalah trafik yang benar-benar sedang mengalir.
Lalu di mana kita harus mencari spesifikasi? Bukan di wiki. Bukan di Confluence. Tapi di nginx access log.
Ayam dan Telur
Untuk me-refactor legacy, Anda butuh jaring pengaman. Saat Anda mengubah sesuatu, Anda harus segera tahu apakah perilakunya berubah. Jaring pengaman itu adalah test.
Tapi legacy tidak punya test. Untuk menulis test, Anda harus tahu apa yang dilakukan kode itu. Untuk tahu apa yang dilakukan kode, Anda harus membacanya. Begitu Anda membaca, ternyata tidak ada test maupun dokumentasi.
Mana yang lebih dulu, ayam atau telur? Inilah kebuntuan klasik yang dinamai Michael Feathers dalam Working Effectively with Legacy Code. Sebagai jawaban, ia mengajukan characterization test — test yang tidak membekukan apa yang seharusnya dilakukan kode dengan benar, melainkan apa yang saat ini dilakukannya, persis apa adanya. Benar dan salah adalah urusan nanti. Yang penting, perilaku sekarang harus dikunci dulu agar Anda bisa menyentuhnya.
Di zaman Feathers, ini ditulis manual oleh manusia. Panggil fungsinya, lalu salin nilai yang keluar ke dalam expected. Membosankan, lambat, dan karena itu tidak ada yang menyelesaikannya sampai tuntas.
Tapi di level API, “hasil dari memanggil fungsi itu” sudah menumpuk di suatu tempat. Setiap hari, puluhan ribu kali. Di dalam file log.
Log Sebulan adalah Spesifikasi
Jika Anda mengumpulkannya selama sebulan, Anda bisa menangkap hampir seluruh perilaku saat ini dari API legacy.
nginx access log (1 bulan):
endpoint · HTTP method · status code · timing
frekuensi panggilan → prioritas
pola error (401, 422, 500 …)
request/response body (ditangkap lewat middleware atau reverse proxy):
pasangan request/response normal → perilaku yang harus lolos
pasangan request/response error → edge case yang tidak boleh rusak
Jika Anda menggabungkan dua aliran ini, hasilnya diterjemahkan langsung menjadi Hurl integration test. Hurl adalah format yang menuliskan request HTTP dan response yang diharapkan apa adanya dalam teks polos. Satu pasang trafik — “request ini menghasilkan response ini” — persis sama dengan satu blok Hurl.
# POST /api/orders — frekuensi panggilan #3, 12 ribu per hari
POST https://api.example.com/orders
Content-Type: application/json
{ "sku": "A-1024", "qty": 2 }
HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.status" == "pending"
jsonpath "$.total" == 49800
Test ini tidak tahu “bagaimana seharusnya API pesanan bekerja.” Ia hanya tahu “ia sedang bekerja seperti ini sekarang.” Itu sudah cukup. Pada saat refactoring mengubah response ini, lampu merah menyala.
Hal-hal yang otomatis tersari dari log:
- Endpoint mana yang benar-benar dipakai → Endpoint yang dipanggil 0 kali selama sebulan adalah dead code. Kandidat untuk dihapus sebelum refactoring.
- Pola response normal → Regression test dasar.
- Pola error → Edge case nyata yang tak terbayangkan manusia. 422 dan 500 yang diciptakan pengguna sungguhan.
- Frekuensi panggilan → Prioritas test. Mulai dari yang 12 ribu per hari.
Item terakhir penting. Saat manusia menulis test, ia mulai dari happy path yang ia ingat. Trafik tidak punya bias semacam itu. Jalur yang benar-benar menanggung beban itulah yang menjadi prioritas.
Jaring Pengaman Dua Lapis
Pendekatan ini bukan dipakai sendirian, melainkan satu lapisan dari pipeline ratchet yang mengangkat legacy menjadi agent-operable.
nginx log (1 bulan) → generasi Hurl otomatis → membekukan perilaku API legacy saat ini
↓
tsma → jaring pengaman tingkat fungsi (unit)
↓
filefunc → merapikan struktur kode (satu konsep satu file)
↓
refactoring → Hurl memverifikasi pelestarian perilaku API (integration)
Intinya, jaring pengaman itu dua lapis.
- tsma = jaring pengaman tingkat fungsi. Menangkap apakah logika internal berubah. Tapi meski signature fungsi tetap sama, perilaku keseluruhan endpoint bisa saja berubah.
- Hurl from traffic = jaring pengaman tingkat API. Menangkap apakah kontrak yang terlihat dari luar tetap terjaga. Bagaimanapun Anda menjungkirbalikkan bagian dalam, asalkan apa yang masuk dari luar dan keluar ke luar tetap sama, ia lolos.
Refactoring menurut definisinya adalah “mengubah struktur internal sambil melestarikan perilaku eksternal.” Maka definisi “perilaku eksternal” yang harus dilestarikan itu harus dibekukan di suatu tempat. tsma menjaga batas dalam, Hurl menjaga batas luar. Hanya ketika dua lapis ini hadir bersama, Anda akhirnya bisa berkata kepada agent: “jungkirbalikkan sepuasnya, mesin yang akan melihat di mana yang rusak.”
Hakim yang Tak Bisa Menjilat
Ini persis berkaitan dengan inti dari Symbolic Feedback Loop.
Jika Anda bertanya kepada agent “apakah refactoring-nya bagus?”, ia menjawab “Ya, sudah saya rapikan dengan bersih.” Berikan opini, ia akan menjilat. Tapi jika Anda menjalankan Hurl, yang keluar adalah POST /orders → expected 201, got 500. Angka dan status code tidak bisa menjilat. Karena mereka tak punya emosi.
Hurl test yang disari dari trafik adalah spesifikasi tanpa intervensi penilaian manusia. Bukan “seseorang berpikir ia harus bekerja seperti ini,” melainkan “hasil observasi menunjukkan ia bekerja seperti ini.” Bukan klaim, melainkan pengukuran. Karena itu benar-salahnya refactoring bisa dihakimi oleh mesin, bukan manusia. LLM bukan hakim melainkan eksekutor, dan penghakiman dilakukan oleh alat deterministik.
Premisnya Hanya Satu: Log yang Tercatat dengan Baik
Agar metode ini berhasil, hanya satu yang dibutuhkan. Log sebulan yang tercatat dengan baik.
Di sini “tercatat dengan baik” adalah segalanya. Access log saja tidak cukup. Ia memberi endpoint, status code, dan timing, tapi tidak memberi inti yang harus dibekukan — pasangan request body dan response body. Hanya tahu POST /orders → 201 tidak memungkinkan Anda mereproduksi “input ini menghasilkan output ini.” Untuk mengunci fungsi, Anda harus memegang baik apa yang masuk maupun apa yang keluar.
Jadi pertanyaan sesungguhnya bukan “bagaimana cara menulis test,” melainkan “apakah log saya tercatat cukup baik untuk menjadi spesifikasi.”
- Apakah request/response body tersimpan, atau hanya status code?
- Apakah response error juga tersimpan? Justru body dari 422 dan 500 itulah edge case yang tak terbayangkan manusia.
- Apakah log terstruktur sehingga mesin bisa memasangkan request dan response?
Jika ini sudah terpenuhi, berarti Anda sebenarnya sudah menulis spesifikasi selama sebulan. Anda tidak perlu menulis test terpisah. Pipeline log-lah yang menulisnya menggantikan Anda. Jika belum terpenuhi, cukup sisipkan satu lapis middleware sekarang dan biarkan menyala selama sebulan. Sebulan kemudian, di tangan Anda akan tergenggam seluruh perilaku legacy saat ini.
Mengapa sebulan, bukan sehari? Sehari hanya menangkap happy path. Sebulan menangkap batch akhir bulan, lonjakan trafik menjelang settlement, endpoint admin yang jarang sekali dipanggil, hingga cron yang berjalan sekali pada pukul 3 dini hari — ekor panjang dari sistem. Spesifikasi bukanlah rata-rata, melainkan distribusi.
Menerjemahkan Log ke Hurl untuk Mengunci Fungsi
Setelah log siap, sisanya bersifat mekanis. Masukkan pasangan request/response sebulan ke dalam alat, dan terjemahkan setiap pasang menjadi blok Hurl. Ratusan file Hurl yang tertuang seperti itu menjadi characterization suite — jaring pengaman yang membekukan seluruh perilaku legacy saat ini. Anda tidak membaca satu baris kode pun. Anda hanya membaca trafik yang telah mengalir.
Mari kita tunjuk dulu satu titik di mana orang sering ragu. “Di log ada data pribadi, pembayaran, dan token — bolehkah itu dibekukan menjadi test?”
Boleh. Lebih tepatnya, tidak perlu dibekukan. Karena metodologi ini pada dasarnya tidak membutuhkan nilai. Yang dikunci oleh characterization test bukan nilai, melainkan perilaku.
HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.total" == 49800
Yang penting sebagai spesifikasi di sini bukanlah angka 49800, melainkan struktur “field total ada sebagai bilangan bulat, dan untuk input tertentu ia dihitung seperti ini.” Meski Anda mask nilainya atau menggantinya dengan data sintetis, nilai spesifikasinya nyaris tidak berkurang. capture → masking → generasi Hurl, seluruh pipeline ini berputar di dalam infrastruktur Anda sendiri. Raw log tidak akan keluar ke mana pun. Yang tersisa hanyalah spesifikasi dengan nilai yang tertutup, kontrak yang hanya melestarikan struktur. Bahwa log tidak perlu dikirim ke luar bukanlah konsesi demi keamanan, melainkan esensi dari pendekatan ini — sebab sejak awal yang perlu dibekukan hanyalah perilaku.
Jika Anda menjalankan Hurl yang dihasilkan sekali di staging, di tempat itu juga lolos/gagalnya terpilah. Jika semua lampu hijau menyala, sekarang Anda bisa mulai refactoring. Suruh agent menjungkirbalikkan sepuasnya, dan Hurl yang melihat di mana yang rusak.
Tangga yang Dipasang Tanpa Kode
Maka nilai sejati dari pendekatan ini bukanlah “menulis test dengan cepat.” Nilai sejatinya adalah ini.
- Bisa dimulai tanpa membaca kode — Penulis aslinya sudah pergi dan dokumentasi tidak ada, namun jaring pengaman terpasang hanya dari trafik yang telah mengalir. Anda mendapat hak untuk menyentuh sebelum memahami kode.
- Hasilnya bisa langsung diverifikasi — Jalankan Hurl yang dihasilkan di staging, dan di tempat itu juga keluar pass/fail. Bukan “semoga berhasil,” melainkan “sekarang 327 dari 327 lolos.”
- Data tidak melewati pagar — Dari capture hingga generasi Hurl, semuanya selesai di dalam infrastruktur saya. Semakin industri itu teregulasi, semakin menentukan fakta bahwa Anda bisa mulai tanpa mengirim apa pun ke luar.
Sekop pertama modernisasi legacy biasanya berhenti di tebing “tidak ada yang tahu apa perilaku saat ini.” Trafik → Hurl memasang tangga di tebing itu. Untuk memasang tangga, kode tidak diperlukan. Cukup trafik yang telah mengalir — dan bahkan trafik itu pun tetap dibiarkan di dalam pagar.
Aliran Itu Sudah Menulis Spesifikasi Sejak Awal
Kita berjuang keras untuk menulis spesifikasi secara terpisah. Kita menulis OpenAPI dengan tangan, mendeskripsikan perilaku di wiki, dan ketika semuanya melenceng dari kode, kita menyebutnya drift sambil mengeluh.
Tapi sistem yang hidup setiap saat sedang menulis spesifikasinya sendiri. Setiap kali satu request masuk dan satu response keluar, itulah satu baris deskripsi diri “aku adalah sistem seperti ini.” File log adalah otobiografi itu yang menumpuk selama sebulan.
Hanya saja kita tidak membacanya.
Legacy bukannya tidak punya dokumentasi. Dokumentasinya ada di dalam access log, hanya saja formatnya tidak nyaman dibaca manusia. Jika diterjemahkan ke Hurl, ia menjadi spesifikasi yang bisa dijalankan, kontrak yang dihakimi mesin.
Dokumentasi berbohong. Trafik tidak berbohong.
Sumber / Dasar
Konsep · alat inti
- Michael Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004. — Sumber konsep characterization test. “Kunci bukan apa yang seharusnya dilakukan kode dengan benar, melainkan apa yang dilakukannya saat ini.”
- Proyek Hurl (hurl.dev) — Format test request/response HTTP dalam teks polos. Diintegrasikan sebagai salah satu dari 10 SSOT yongol.
- Pembuktian tsma pada 527 fungsi — ratchet tingkat fungsi (tsma).
Menyari test dari trafik · eksekusi (carving / record-replay)
- Elbaum, Chin, Dwyer, Jorde (2009). “Carving and Replaying Differential Unit Test Cases from System Test Cases.” IEEE TSE 35(1). — Dasar akademik differential unit test yang me-record eksekusi sistem lalu me-replay-nya pada tingkat unit.
- Tim Engineering Meta (2024). “Observation-based Unit Test Generation at Meta.” FSE 2024, arXiv:2402.06111. — Carving test dari nilai observasi eksekusi aplikasi. 9,6 juta kali eksekusi di CI, mendeteksi 5.702 cacat. Pembuktian skala industri dari “observasi adalah test.”
Membekukan perilaku saat ini (snapshot / golden master)
- Fujita, Kashiwa, Lin, Iida (2023). “An Empirical Study on the Use of Snapshot Testing.” ICSME 2023. — Pembuktian adopsi test snapshot (= golden master/characterization). “Mengunci output saat ini, bukan kebenaran, untuk mendeteksi perubahan.”
Jaring pengaman refactoring
- Kim, Zimmermann, Nagappan (2014). “An Empirical Study of Refactoring Challenges and Benefits at Microsoft.” IEEE TSE 40(7). — Pembuktian bahwa tanpa test yang menjamin pelestarian perilaku, refactoring adalah biaya sekaligus risiko.
- Yoo, Harman (2012). “Regression Testing Minimization, Selection and Prioritization: A Survey.” STVR 22(2). — Regression test = definisi standar “keyakinan bahwa perubahan tidak merusak perilaku yang ada.”
Distribusi penggunaan nyata adalah prioritas
- John D. Musa (1993). “Operational Profiles in Software-Reliability Engineering.” IEEE Software 10(2). — Jika test dialokasikan berdasarkan frekuensi penggunaan, maka meski dihentikan karena jadwal, fungsi yang paling banyak dipakai paling banyak terverifikasi. Dasar klasik “distribusi trafik alih-alih bias happy path.”
Mengapa mesin yang harus menghakimi (LLM bukan hakim melainkan eksekutor)
Huang, Chen, Mishra, et al. (2024). “Large Language Models Cannot Self-Correct Reasoning Yet.” ICLR 2024, arXiv:2310.01798. — Tanpa umpan balik eksternal, LLM tidak bisa mengoreksi penalarannya sendiri. Alasan mengapa verifier eksternal deterministik dibutuhkan.
Sharma, Tong, et al. (2024). “Towards Understanding Sycophancy in Language Models.” ICLR 2024, arXiv:2310.13548. — RLHF mengajarkan penyesuaian diri sehingga meruntuhkan keandalan penghakiman diri LLM.
Gambar sampul: dihasilkan AI (Google Gemini)
Bacaan Pendamping
- Michael Feathers, “Characterization Testing” — Tulisan pencipta istilah ini. “Pada saat software masuk ke produksi, ia menjadi spesifikasinya sendiri (it becomes its own specification).” Tesis yang nyaris sama dengan judul tulisan ini.
- Tutorial resmi Hurl, “Your First Hurl File” — Dari
GET / HTTP 200hingga mode--test. Pengantar yang membuat Anda langsung menggenggam bahwa satu baris teks polos adalah sebuah test. - GitHub Engineering, “Scientist: Measure Twice, Cut Once” — Library yang menjalankan kode legacy (control) dan baru (candidate) secara bersamaan di produksi lalu membandingkan hasilnya. “Hanya perilaku nyata yang merupakan spesifikasi sesungguhnya.”
- Twitter Diffy (rangkuman InfoQ) — Proxy yang mengirim request yang sama ke layanan baru/lama lalu menangkap hanya perbedaan response sebagai regresi. Preseden klasik dari “membekukan perilaku tanpa menulis test.”
- GoReplay — Alat yang menangkap trafik HTTP live dari network interface lalu me-replay-nya ke staging. Implementasi representatif dari “trafik produksi sebagai input test.”
- Nicolas Carlo, “Characterization vs Approval Tests” — Merapikan tiga istilah untuk teknik yang sebenarnya sama, dan menekankan peran “Printer” yang men-scrub informasi sensitif dari output.
- Pact — consumer-driven contract testing. Pendekatan “kontrak eksplisit” yang berlawanan dengan pembekuan trafik. Melihat kedua cara bersama memberi keseimbangan.
Tulisan Terkait
- Hurl Mencegah Drift — Cara mendeklarasikan kontrak HTTP dalam teks polos dan menguncinya di CI. Jika tulisan ini adalah “trafik → Hurl”, maka itu adalah “mengunci drift dengan Hurl”.
- tsma — Garis Pertahanan Regresi untuk Kode Legacy — Batas dalam (tingkat fungsi) dari jaring pengaman dua lapis. Jika Hurl adalah batas luar, maka tsma adalah batas dalam.
- Agent Operable Codebase — Pipeline 3 tahap yang mengangkat legacy menjadi kode yang bisa dikerjakan agent.
- Mengapa Agent Coding Bekerja dan Mengapa Runtuh — Struktur dari Symbolic Feedback Loop.
- Constraint adalah Kontrak — Test sebagai kontrak yang bisa diverifikasi dan dipaksakan.
- Cara Menyelamatkan Vibe Coding yang Gagal — Kuliah praktis untuk mendiagnosis → mengunci → memperbaiki → mengekstrak → mengonversi legacy dengan characterization testing.