Refactor pada aplikasi JavaScript legacy sering gagal bukan karena perubahan terlalu besar, tetapi karena tim tidak punya safety net yang cukup untuk membedakan bug lama, perilaku yang sengaja dipertahankan, dan regresi baru. Strategi uji legacy JavaScript yang efektif bukan dimulai dari mengejar test coverage tinggi, melainkan dari memahami area berisiko lalu menulis pengujian yang menjaga perilaku paling penting tetap stabil.
Untuk tim yang merawat kode warisan, pendekatan yang paling berguna biasanya terdiri dari lima langkah: memetakan area risiko, menulis characterization test sebelum refactor, membedakan unit/integration/e2e berdasarkan nilai deteksi regresi, mengisolasi flaky test, lalu menyusun workflow verifikasi di CI agar perubahan berbahaya cepat terdeteksi. Konteks ini sejalan dengan beban yang sering dibahas di komunitas dev.to tentang legacy: masalahnya bukan hanya kode tua, tetapi ketidakpastian saat menyentuhnya.
Mengapa legacy JavaScript rawan regresi saat refactor
Pada banyak codebase legacy, perilaku sistem tidak sepenuhnya didokumentasikan. Ada logika bisnis tersembunyi di event handler frontend, utilitas global, callback bertingkat, mutasi objek bersama, atau endpoint backend yang diam-diam diandalkan klien tertentu. Saat refactor dilakukan hanya berdasarkan pembacaan kode, tim sering menghapus perilaku yang terlihat aneh padahal sebenarnya dipakai.
Masalah khas pada JavaScript legacy meliputi:
- State implisit: nilai disimpan di modul global,
window, cache proses, atau singleton. - Ketergantungan kuat pada waktu dan lingkungan: timezone, locale, urutan event, network, browser API, atau jam sistem.
- Boundary yang kabur: fungsi kecil memanggil I/O langsung, sulit diisolasi sebagai unit test.
- Kontrak tidak tertulis: format respons API, shape objek, side effect DOM, atau urutan pemanggilan callback.
Karena itu, tujuan awal pengujian bukan “membuktikan kode bersih”, tetapi mengunci perilaku yang harus tetap sama selama refactor bertahap.
1) Petakan area berisiko sebelum menulis test
Jangan mulai dari file terbesar atau modul paling jelek. Mulailah dari area yang paling berisiko memicu insiden bila berubah. Pemetaan risiko membantu tim memutuskan test mana yang harus ditulis lebih dulu.
Kriteria memetakan risiko
- Frekuensi perubahan tinggi: modul sering disentuh pada sprint sebelumnya.
- Dampak bisnis tinggi: checkout, autentikasi, pricing, invoice, sinkronisasi data, notifikasi.
- Banyak dependensi: modul dipakai banyak fitur lain.
- Riwayat bug tinggi: file atau endpoint yang sering memicu hotfix.
- Observability lemah: error tidak jelas di log, sulit direproduksi lokal.
Praktik sederhana untuk membuat peta risiko
- Ambil 20-30 file atau endpoint yang paling sering berubah dari riwayat Git.
- Gabungkan dengan daftar insiden, bug support, dan area bisnis kritikal.
- Beri skor sederhana, misalnya 1-5, untuk impact, change frequency, dan unknown behavior.
- Prioritaskan modul dengan skor total tertinggi sebagai target characterization test.
Contoh kategori target prioritas pada aplikasi legacy JavaScript:
- Frontend: formatter harga, alur submit form, filtering tabel, persistensi state URL, komponen yang bergantung pada event DOM lama.
- Backend: normalisasi payload request, middleware otorisasi, mapping respons API pihak ketiga, scheduler, dan modul perhitungan.
Catatan: Area berisiko tidak selalu area paling kompleks. Kadang helper sederhana yang dipakai di 40 tempat jauh lebih berbahaya untuk diubah daripada satu file besar yang terisolasi.
2) Tulis characterization test sebelum refactor
Characterization test bertujuan merekam perilaku sistem saat ini, termasuk perilaku yang mungkin belum ideal. Ini berbeda dari test desain baru yang mengekspresikan perilaku ideal. Pada legacy, langkah pertama biasanya bukan memperbaiki desain, melainkan memastikan Anda tahu apa yang berubah.
Kapan characterization test tepat digunakan
- Saat fungsi sulit dipahami tetapi sudah dipakai produksi.
- Saat Anda curiga ada banyak kasus tepi yang tidak terdokumentasi.
- Saat refactor harus dilakukan cepat tanpa menulis ulang modul dari nol.
Prinsip menulis characterization test
- Uji input-output dan side effect yang benar-benar terjadi sekarang.
- Mulai dari kasus yang paling sering terjadi di produksi.
- Jangan langsung “memperbaiki” perilaku aneh tanpa persetujuan bisnis atau bukti aman.
- Jika hasilnya jelas salah, tandai sebagai known bug dan pisahkan dari test perilaku yang harus dipertahankan.
Contoh frontend: utilitas filter dan sort pada tabel legacy
Misalkan ada modul lama yang memfilter data produk dan digunakan banyak halaman. Kodenya sulit dibaca, tapi tim ingin memecahnya.
export function filterProducts(items, query) {
return items
.filter((item) => {
if (!query) return true;
return String(item.name || '').toLowerCase().includes(query.toLowerCase())
|| String(item.sku || '').includes(query);
})
.sort((a, b) => {
if (a.stock === 0 && b.stock > 0) return 1;
if (b.stock === 0 && a.stock > 0) return -1;
return String(a.name).localeCompare(String(b.name));
});
}Sebelum refactor, tulis test yang mengunci perilaku penting:
import { filterProducts } from './filterProducts';
describe('filterProducts legacy behavior', () => {
it('mengembalikan semua item saat query kosong', () => {
const items = [
{ name: 'Keyboard', sku: 'KB-01', stock: 1 },
{ name: 'Mouse', sku: 'MS-02', stock: 0 }
];
expect(filterProducts(items, '')).toEqual([
{ name: 'Keyboard', sku: 'KB-01', stock: 1 },
{ name: 'Mouse', sku: 'MS-02', stock: 0 }
]);
});
it('mencocokkan query ke name tanpa membedakan huruf besar kecil', () => {
const items = [{ name: 'Keyboard', sku: 'KB-01', stock: 1 }];
expect(filterProducts(items, 'key')).toHaveLength(1);
});
it('mendorong item out-of-stock ke urutan belakang', () => {
const items = [
{ name: 'Mouse', sku: 'MS-02', stock: 0 },
{ name: 'Keyboard', sku: 'KB-01', stock: 3 }
];
expect(filterProducts(items, '')[0].name).toBe('Keyboard');
});
});Test seperti ini tidak mencoba membuktikan implementasi “bagus”, tetapi menangkap kontrak perilaku yang akan rusak bila refactor salah.
Contoh backend: normalisasi payload webhook
Pada backend Node.js, modul legacy sering menerima payload dari pihak ketiga yang tidak konsisten. Refactor tanpa characterization test bisa memutus kompatibilitas.
export function normalizeWebhook(payload) {
return {
id: payload.id || payload.event_id || null,
type: (payload.type || payload.eventType || 'unknown').toLowerCase(),
amount: Number(payload.amount || 0),
customerEmail: payload.customer?.email || payload.email || null
};
}Test yang berguna:
import { normalizeWebhook } from './normalizeWebhook';
describe('normalizeWebhook current behavior', () => {
it('menerima variasi field dari provider lama', () => {
expect(normalizeWebhook({
event_id: 'evt-1',
eventType: 'PAID',
amount: '15000',
email: '[email protected]'
})).toEqual({
id: 'evt-1',
type: 'paid',
amount: 15000,
customerEmail: '[email protected]'
});
});
it('menghasilkan nilai fallback yang stabil saat field tidak ada', () => {
expect(normalizeWebhook({})).toEqual({
id: null,
type: 'unknown',
amount: 0,
customerEmail: null
});
});
});Pola ini memberi dasar aman sebelum Anda memecah fungsi menjadi parser, validator, dan mapper terpisah.
3) Bedakan unit, integration, dan e2e berdasarkan nilai, bukan kebiasaan
Pada legacy, kesalahan umum adalah menulis terlalu banyak test pada level yang salah. Hasilnya bisa dua ekstrem: unit test sangat banyak tetapi tidak menangkap regresi nyata, atau e2e terlalu banyak hingga lambat dan rapuh.
Unit test: pilih untuk logika deterministik yang bisa diisolasi
Cocok untuk:
- Perhitungan, formatter, parser, mapper.
- Fungsi dengan input-output jelas dan minim I/O.
- Kasus tepi yang sulit dijangkau dari UI atau API penuh.
Kurang cocok untuk:
- Kode yang sangat terikat ke DOM, timer, atau database tanpa boundary jelas.
- Modul yang nilainya justru ada pada interaksi antar komponen.
Integration test: paling bernilai untuk banyak codebase legacy
Pada aplikasi legacy JavaScript, integration test sering memberi rasio biaya-manfaat terbaik. Test ini memverifikasi beberapa komponen bekerja bersama: route dengan service dan database test, atau komponen UI dengan state dan API mock yang realistis.
Cocok untuk:
- Endpoint backend yang punya validasi, transformasi, dan persistence.
- Form frontend yang bergantung pada state, event, dan respons API.
- Kasus di mana bug historis muncul dari interaksi, bukan dari satu fungsi murni.
E2E test: sedikit, fokus pada alur kritikal
E2E tetap penting, tetapi sebaiknya dipakai hemat untuk jalur bernilai tinggi:
- Login
- Checkout atau pembayaran
- Pembuatan order atau submission form penting
- Hak akses dasar
Jika semua regresi ingin ditangkap lewat E2E, suite akan lambat, sulit dipelihara, dan rawan false positive.
Panduan memilih level test untuk satu skenario
Misalnya ada bug pada form alamat pelanggan di frontend:
- Jika bug ada pada fungsi normalisasi kode pos, tulis unit test.
- Jika bug muncul saat form submit dan payload API salah, tulis integration test komponen + mock API.
- Jika bug terkait alur pengguna lengkap dari login sampai checkout, tambahkan satu E2E pada jalur utama.
Prinsipnya: uji pada level terendah yang masih bisa menangkap regresi nyata.
4) Isolasi flaky test secepat mungkin
Flaky test sangat berbahaya pada codebase legacy karena menurunkan kepercayaan tim terhadap hasil CI. Begitu tim terbiasa melihat build merah palsu, regresi asli jadi mudah lolos.
Penyebab flaky test yang umum di JavaScript
- Ketergantungan pada waktu nyata, seperti
Date.now()atau timer async. - Race condition pada event loop, promise, atau rendering UI.
- State global bocor antar test.
- Data uji berbagi resource yang sama, misalnya database/test account yang tidak diisolasi.
- Ketergantungan pada network eksternal atau service yang tidak stabil.
Strategi isolasi
- Tandai dan pisahkan test flaky dari jalur merge utama bila belum sempat diperbaiki.
- Hilangkan sumber nondeterministik dengan mock waktu, seed data tetap, dan stub network.
- Reset state setelah tiap test: DOM, cache modul, database, environment variable, dan mock.
- Jangan menambah retry sebagai solusi utama. Retry boleh membantu diagnosis, tetapi bukan pengganti akar masalah.
Contoh perbaikan sumber nondeterministik
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01T00:00:00Z'));
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});Contoh ini berguna bila logika legacy bergantung pada tanggal atau timeout. Jika framework test berbeda, prinsipnya tetap sama: buat waktu dan state dapat diprediksi.
Catatan: Jangan menyembunyikan flaky test dengan menandainya permanen sebagai skip tanpa tiket tindak lanjut. Itu hanya memindahkan risiko ke produksi.
5) Susun workflow verifikasi di CI agar regresi cepat terdeteksi
Strategi uji legacy JavaScript tidak lengkap tanpa workflow CI yang jelas. Tujuannya bukan hanya menjalankan semua test, tetapi memberikan umpan balik cepat di titik yang tepat.
Urutan verifikasi yang praktis
- Fast checks: lint, type check bila ada, dan unit test cepat.
- Integration tests untuk area yang terdampak perubahan.
- Smoke E2E untuk jalur kritikal utama.
- Suite penuh terjadwal atau pada branch tertentu bila runtime terlalu besar untuk setiap commit.
Prinsip penyusunan pipeline
- Fail fast: error sintaks, lint, dan test cepat harus dieksekusi lebih dulu.
- Pisahkan kategori test: memudahkan analisis apakah kegagalan ada di unit, integrasi, atau E2E.
- Publikasikan artefak debugging: log, screenshot, trace, atau rekaman respons API mock bila tersedia.
- Jangan campur test stabil dan test karantina dalam syarat merge utama.
Contoh alur CI yang realistis
Untuk pull request yang menyentuh modul frontend/backend legacy:
- Jalankan lint dan test unit yang relevan.
- Jalankan integration test pada paket/aplikasi yang berubah.
- Jalankan smoke E2E untuk login dan satu alur bisnis utama.
- Jika merge ke branch utama, jalankan suite lebih lengkap secara paralel bila memungkinkan.
Bila repository besar, tim dapat memakai strategi test selection berbasis folder terdampak atau tag suite. Namun hati-hati: pemetaan dependensi yang tidak akurat bisa membuat regresi lolos. Jika belum yakin dengan mekanisme seleksi, prioritaskan subset yang eksplisit dan konservatif.
Checklist prioritas untuk tim yang baru mulai
Berikut urutan kerja yang bisa langsung diterapkan dalam 1-2 iterasi sprint:
- Identifikasi 3-5 area risiko tertinggi dari bug history, log produksi, dan riwayat perubahan Git.
- Tulis characterization test untuk perilaku paling penting sebelum file disentuh.
- Tambahkan integration test pada boundary nyata: route-service-db atau form-state-api.
- Pilih 1-3 E2E kritikal saja untuk syarat merge awal.
- Karantina flaky test dan buat tiket perbaikannya.
- Atur CI fail-fast dengan pemisahan unit, integration, dan E2E.
- Ukur kualitas lewat stabilitas dan deteksi regresi, bukan hanya persentase coverage.
Trade-off biaya vs coverage
Pada legacy, coverage tinggi tidak otomatis berarti aman untuk refactor. Biaya menulis dan merawat test harus sebanding dengan nilai deteksi regresi.
Trade-off yang perlu dipahami
- Unit test murah dan cepat, tetapi bisa gagal menangkap masalah integrasi nyata.
- Integration test lebih lambat, tetapi sering paling representatif untuk legacy yang boundary-nya kabur.
- E2E paling mahal dalam runtime dan maintenance, tetapi penting untuk jalur kritikal pengguna.
Karena itu, target yang sehat biasanya bukan “sebanyak mungkin test”, melainkan kombinasi test yang menutup risiko terbesar dengan biaya operasional yang masih masuk akal.
Tanda distribusi test Anda kurang sehat:
- Coverage tinggi, tetapi bug integrasi terus lolos.
- E2E banyak, tetapi CI terlalu lambat untuk dipakai sebagai umpan balik harian.
- Tim takut mengubah kode karena test gagal tidak jelas penyebabnya.
Kapan test perlu ditulis ulang, bukan ditambal
Tidak semua test legacy layak dipertahankan. Ada saatnya test perlu ditulis ulang agar benar-benar membantu refactor.
Tanda test perlu ditulis ulang
- Terlalu terikat implementasi internal: setiap refactor kecil memecahkan test meski perilaku tetap benar.
- Assertion terlalu lemah: test lewat tetapi tidak memverifikasi hasil penting.
- Setup sangat kompleks hingga sulit dipahami lebih dari kode produksinya.
- Sering flaky dan akar masalahnya ada pada desain test, bukan pada sistem.
- Duplikasi besar yang membuat perubahan satu kontrak butuh edit banyak file test.
Menulis ulang test bukan berarti kehilangan perlindungan. Lakukan bertahap: pertahankan perlindungan minimal di level integrasi, lalu ganti test rapuh dengan versi yang lebih fokus pada kontrak.
Anti-pattern umum saat menambal legacy tanpa safety net
- Refactor besar sekaligus tanpa baseline perilaku.
- Mengandalkan manual testing saja untuk modul yang punya banyak kombinasi input.
- Mock berlebihan hingga test tidak lagi mewakili integrasi nyata.
- Mengejar coverage angka alih-alih menutup area risiko.
- Menyamakan bug lama dengan spesifikasi resmi tanpa validasi bisnis.
- Mencampur perbaikan perilaku dan refactor struktural dalam satu perubahan besar.
- Membiarkan flaky test hidup lama di pipeline utama.
Anti-pattern paling mahal adalah memperbaiki desain internal sambil diam-diam mengubah kontrak eksternal. Jika perubahan perilaku memang diinginkan, nyatakan eksplisit di PR dan ubah test sebagai bagian dari keputusan bisnis, bukan sebagai efek samping refactor.
Playbook ringkas yang bisa diterapkan minggu ini
- Pilih satu modul legacy yang sering bermasalah.
- Petakan input, output, side effect, dan konsumen utamanya.
- Tulis 3-5 characterization test untuk perilaku paling kritikal.
- Tambahkan satu integration test di boundary yang paling sering rusak.
- Refactor kecil-kecil, jalankan CI fail-fast, dan amati perubahan perilaku.
- Jika ada test tidak stabil, karantina lalu perbaiki sumber nondeterministiknya.
Dengan pendekatan ini, tim tidak perlu menunggu penulisan ulang total atau coverage sempurna untuk mulai aman menyentuh legacy. Yang dibutuhkan adalah strategi uji legacy JavaScript yang fokus pada risiko nyata, perilaku yang harus dipertahankan, dan alur verifikasi yang memberi sinyal cepat saat regresi muncul.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!