TESTING REFACTORING LEGACY-CODE SOFTWARE-QUALITY SOFTWARE-DEVELOPMENT BEST-PRACTICES QUALITY-ASSURANCE DEVELOPER-EXPERIENCE CODE-QUALITY TEST-AUTOMATION

Jurus Rahasia Refactoring Kode Warisan: Menggali Golden Master Testing

⏱️ 15 menit baca
👨‍💻

Jurus Rahasia Refactoring Kode Warisan: Menggali Golden Master Testing

1. Pendahuluan

Pernahkah Anda merasa ngeri saat harus mengubah sebaris kode di aplikasi warisan (legacy application) yang sudah berjalan bertahun-tahun? Kode yang tidak memiliki tes, penuh dengan logika bisnis yang misterius, dan terasa seperti bom waktu yang siap meledak kapan saja? Anda tidak sendirian. Ini adalah skenario umum yang dihadapi banyak developer.

Refactoring kode seperti ini adalah kebutuhan mutlak untuk menjaga aplikasi tetap sehat, scalable, dan mudah dirawat. Namun, melakukannya tanpa jaring pengaman yang memadai adalah tindakan bunuh diri. Bagaimana kita bisa yakin bahwa perubahan yang kita buat tidak merusak fungsionalitas yang sudah ada, terutama jika tidak ada dokumentasi yang jelas atau orang yang memahami sepenuhnya semua side effect dari kode tersebut?

Di sinilah Golden Master Testing, atau sering juga disebut Characterization Testing, datang sebagai pahlawan tanpa tanda jasa. Ini adalah teknik yang memungkinkan kita untuk mulai merefaktor kode warisan dengan percaya diri, bahkan ketika kita tidak memiliki satu pun tes yang ditulis sebelumnya. Mari kita selami lebih dalam bagaimana jurus rahasia ini bisa menyelamatkan Anda dari mimpi buruk kode legacy.

2. Apa Itu Golden Master Testing?

📌 Golden Master Testing (GMT) adalah sebuah pendekatan testing di mana kita menangkap perilaku yang ada dari sebuah sistem atau bagian kode yang kompleks, lalu menggunakan output yang ditangkap tersebut sebagai “golden master” atau baseline. Kemudian, setiap perubahan yang kita buat pada kode akan dibandingkan dengan golden master ini. Jika output setelah perubahan berbeda dari golden master, maka tes gagal, menandakan bahwa perilaku sistem telah berubah.

💡 Tujuan utama GMT bukanlah untuk memverifikasi kebenaran atau spesifikasi kode (karena spesifikasi itu mungkin tidak pernah ada!), melainkan untuk memastikan bahwa perilaku yang ada dari kode tersebut tidak berubah secara tidak sengaja selama proses refactoring. Ini sangat krusial saat kita berhadapan dengan kode yang sudah bekerja di produksi, tetapi kita tidak tahu mengapa atau bagaimana persisnya ia bekerja.

Bayangkan Anda memiliki sebuah mesin tua yang sangat penting. Mesin itu bekerja dengan baik selama bertahun-tahun, tapi tidak ada blueprint atau manual instruksi. Anda ingin memodifikasi salah satu bagiannya agar lebih efisien. Sebelum membongkar, Anda akan mencatat setiap detail perilaku mesin: suara yang dihasilkan, getaran, kecepatan putaran, dan semua output yang keluar. Catatan detail ini adalah “golden master” Anda. Ketika Anda memodifikasi mesin, Anda akan terus membandingkan perilaku barunya dengan catatan “golden master” Anda. Jika ada perbedaan yang tidak disengaja, Anda tahu ada yang salah.

GMT berfokus pada “apa yang kode lakukan sekarang” daripada “apa yang kode seharusnya lakukan”. Ini adalah langkah awal untuk mendapatkan kontrol atas kode warisan, sebelum kita bisa mulai menulis unit test yang lebih spesifik dan memvalidasi kebenaran perilaku.

3. Kapan Menggunakan Golden Master Testing?

GMT adalah alat yang sangat spesifik dan paling efektif dalam skenario tertentu.

🎯 Kasus Ideal untuk Golden Master Testing:

Kapan GMT Bukan Solusi Utama (atau satu-satunya):

GMT adalah alat yang sangat berharga untuk menciptakan jaring pengaman sementara yang memungkinkan Anda merefaktor dengan aman. Ini adalah investasi awal yang akan sangat menguntungkan di kemudian hari, terutama saat Anda berjuang untuk mendapatkan pemahaman dan kontrol atas kode warisan yang menakutkan.

4. Langkah-langkah Menerapkan Golden Master Testing

Menerapkan Golden Master Testing cukup lugas, meskipun membutuhkan sedikit ketelitian di awal. Berikut adalah langkah-langkah praktisnya:

4.1. Identifikasi Target Refactoring

Pilih bagian kode yang paling ingin Anda refactor. Ini bisa berupa satu fungsi, sebuah kelas, atau bahkan sebuah modul. Mulailah dari yang kecil dan paling kritikal.

4.2. Buat “Golden Master”

Ini adalah langkah paling penting. Anda perlu menjalankan kode target dengan serangkaian input yang representatif dan menangkap semua outputnya.

  1. Siapkan Input Representatif:

    • Pikirkan berbagai skenario penggunaan kode Anda.
    • Sertakan input “normal”, “edge case” (misalnya, nilai nol, string kosong, daftar kosong), dan input yang memicu jalur kode yang berbeda.
    • Semakin banyak variasi input yang Anda siapkan, semakin kuat golden master Anda.
  2. Jalankan Kode dan Tangkap Output:

    • Jalankan kode target dengan setiap input yang telah Anda siapkan.
    • Tangkap outputnya. Output ini bisa beragam:
      • String/JSON: Jika kode menghasilkan data terstruktur.
      • File: Jika kode menghasilkan file (laporan, gambar, dll.).
      • Screenshot: Untuk komponen UI atau output visual.
      • Log: Jika perilaku penting tercatat di log.
      • Database State: Perubahan pada database (meskipun ini lebih kompleks).
    • Simpan output yang ditangkap ini sebagai “golden master” Anda. Idealnya, simpan dalam format yang mudah dibaca dan dibandingkan (misalnya, file .json, .txt, atau .snapshot).

💡 Contoh (Pseudo-code):

// Misalkan ini adalah fungsi legacy yang ingin kita refactor
function processLegacyData(data) {
  // ... logika kompleks dengan banyak side effect ...
  return `Processed: ${JSON.stringify(data).toUpperCase()} - ${new Date().getFullYear()}`; // Contoh output
}

// Input representatif
const inputs = [
  { id: 1, name: "Alice", value: 100 },
  { id: 2, name: "Bob", value: 0 },
  { id: 3, name: "", value: -50 }, // edge case
];

const goldenMasters = {};
inputs.forEach((input, index) => {
  goldenMasters[`input_${index}`] = processLegacyData(input);
});

// goldenMasters sekarang berisi output dari setiap input
// Contoh: { "input_0": "PROCESSED: {\"ID\":1,\"NAME\":\"ALICE\",\"VALUE\":100} - 2023", ... }
// Simpan ini ke file atau variabel untuk perbandingan.

⚠️ Penting: Pastikan output yang Anda tangkap bersifat deterministik. Hindari elemen yang berubah setiap kali dijalankan (misalnya, timestamp, ID acak, atau urutan elemen dalam set yang tidak diurutkan) kecuali jika Anda memiliki cara untuk menormalisasinya.

4.3. Tulis Tes Perbandingan

Setelah Anda memiliki golden master, langkah selanjutnya adalah menulis tes otomatis yang akan membandingkan output kode saat ini dengan golden master yang tersimpan.

  1. Buat Test Case: Untuk setiap input yang Anda gunakan untuk membuat golden master, buatlah sebuah test case.
  2. Jalankan Kode Lagi: Di dalam test case, jalankan kode target dengan input yang sama.
  3. Bandingkan Output: Bandingkan output yang dihasilkan saat ini dengan golden master yang sesuai.

Contoh dengan Jest (JavaScript):

Jest memiliki fitur snapshot testing yang sangat cocok untuk Golden Master Testing.

// __tests__/legacyProcessor.test.js
const processLegacyData = require('../legacyProcessor'); // Asumsikan fungsi di file terpisah

describe('Legacy Data Processor - Golden Master Tests', () => {
  const inputs = [
    { id: 1, name: "Alice", value: 100 },
    { id: 2, name: "Bob", value: 0 },
    { id: 3, name: "", value: -50 },
    { id: 4, name: "Charlie", value: 200, options: { debug: true } }, // Input lebih kompleks
  ];

  // Untuk memastikan output deterministik, kita bisa mock tanggal jika ada di output
  const mockDate = new Date('2023-01-01T00:00:00.000Z');
  const originalDate = global.Date;
  beforeAll(() => {
    global.Date = class extends originalDate {
      constructor(dateString) {
        if (dateString) {
          return new originalDate(dateString);
        }
        return mockDate;
      }
    };
  });
  afterAll(() => {
    global.Date = originalDate;
  });


  inputs.forEach((input, index) => {
    test(`should produce consistent output for input case ${index}`, () => {
      const actualOutput = processLegacyData(input);
      // Ini akan membuat atau membandingkan snapshot dengan file .snap
      expect(actualOutput).toMatchSnapshot();
    });
  });
});

Saat pertama kali Anda menjalankan tes ini (jest), Jest akan membuat file __snapshots__/legacyProcessor.test.js.snap yang berisi “golden master” Anda. Setiap kali Anda menjalankan tes selanjutnya, Jest akan membandingkan output baru dengan snapshot ini.

4.4. Refactor dengan Percaya Diri

Setelah semua tes Golden Master Anda berhasil (hijau), Anda memiliki jaring pengaman! Sekarang Anda bisa mulai merefaktor kode target.

Dengan pendekatan ini, Anda bisa merefaktor kode legacy selangkah demi selangkah, dengan keyakinan bahwa setiap perubahan tidak merusak fungsionalitas yang sudah ada.

5. Contoh Konkret: Refactoring Fungsi Pemformatan Laporan

Mari kita ambil contoh yang lebih praktis. Bayangkan Anda memiliki fungsi lama yang mengambil data pesanan dan memformatnya menjadi string laporan yang kompleks.

// orderFormatter.js (Kode Legacy)
function formatOrderReport(order) {
  let report = `--- Laporan Pesanan #${order.id} ---\n`;
  report += `Nama Pelanggan: ${order.customerName}\n`;
  report += `Tanggal: ${new Date(order.orderDate).toLocaleDateString('id-ID')}\n`; // Ada dependensi tanggal
  report += `Item:\n`;
  let total = 0;
  order.items.forEach(item => {
    report += `  - ${item.name} (${item.qty}x) @ Rp${item.price.toLocaleString('id-ID')}\n`;
    total += item.qty * item.price;
  });
  report += `Total Pembayaran: Rp${total.toLocaleString('id-ID')}\n`;

  // Ada logika diskon aneh yang tersembunyi
  if (order.customerName.includes("VIP")) {
    const discount = total * 0.1;
    report += `Diskon VIP (10%): Rp${discount.toLocaleString('id-ID')}\n`;
    total -= discount;
    report += `Total Setelah Diskon: Rp${total.toLocaleString('id-ID')}\n`;
  }

  report += `Status: ${order.status.toUpperCase()}\n`;
  report += `----------------------------\n`;
  return report;
}

module.exports = formatOrderReport;

Fungsi ini memiliki banyak masalah: dependensi tanggal global, logika diskon yang bercampur, dan sulit dibaca. Kita ingin merefaktornya menjadi lebih modular dan testable.

5.1. Membuat Golden Master

Pertama, siapkan input dan tangkap outputnya.

// orderFormatter.test.js
const formatOrderReport = require('./orderFormatter');

describe('formatOrderReport - Golden Master Tests', () => {
  const mockOrders = [
    {
      id: 'A001',
      customerName: 'Budi Santoso',
      orderDate: '2023-03-15T10:00:00Z',
      items: [
        { name: 'Laptop', qty: 1, price: 12000000 },
        { name: 'Mouse', qty: 2, price: 150000 },
      ],
      status: 'completed',
    },
    {
      id: 'B002',
      customerName: 'Siti VIP', // Memicu diskon VIP
      orderDate: '2023-03-16T11:30:00Z',
      items: [
        { name: 'Keyboard Mekanik', qty: 1, price: 800000 },
      ],
      status: 'pending',
    },
    {
      id: 'C003',
      customerName: 'Joko',
      orderDate: '2023-03-17T09:00:00Z',
      items: [], // Pesanan kosong
      status: 'cancelled',
    },
  ];

  // Mock Date untuk konsistensi di output
  const mockDate = new Date('2023-03-15T00:00:00.000Z');
  const originalDate = global.Date;
  beforeAll(() => {
    global.Date = class extends originalDate {
      constructor(dateString) {
        if (dateString) {
          return new originalDate(dateString);
        }
        return mockDate;
      }
    };
  });
  afterAll(() => {
    global.Date = originalDate;
  });

  mockOrders.forEach((order, index) => {
    test(`should generate consistent report for order case ${index}`, () => {
      const actualReport = formatOrderReport(order);
      expect(actualReport).toMatchSnapshot();
    });
  });
});

Jalankan jest. Ini akan menghasilkan file snapshot yang berisi laporan yang diformat untuk setiap pesanan. File snapshot ini adalah “golden master” kita.

5.2. Refactoring dan Verifikasi

Sekarang, kita bisa mulai merefaktor formatOrderReport. Misalnya, kita ingin memisahkan logika diskon dan pemformatan tanggal.

// orderFormatter.js (Setelah Refactoring Awal)
function calculateDiscount(total, customerName) {
  if (customerName.includes("VIP")) {
    return total * 0.1;
  }
  return 0;
}

function formatCurrency(amount) {
  return `Rp${amount.toLocaleString('id-ID')}`;
}

function formatDate(dateString) {
  return new Date(dateString).toLocaleDateString('id-ID');
}

function formatOrderReport(order) {
  let report = `--- Laporan Pesanan #${order.id} ---\n`;
  report += `Nama Pelanggan: ${order.customerName}\n`;
  report += `Tanggal: ${formatDate(order.orderDate)}\n`; // Menggunakan fungsi baru
  report += `Item:\n`;
  let total = 0;
  order.items.forEach(item => {
    report += `  - ${item.name} (${item.qty}x) @ ${formatCurrency(item.price)}\n`;
    total += item.qty * item.price;
  });
  report += `Total Pembayaran: ${formatCurrency(total)}\n`;

  const discount = calculateDiscount(total, order.customerName); // Menggunakan fungsi baru
  if (discount > 0) {
    report += `Diskon VIP (10%): ${formatCurrency(discount)}\n`;
    total -= discount;
    report += `Total Setelah Diskon: ${formatCurrency(total)}\n`;
  }

  report += `Status: ${order.status.toUpperCase()}\n`;
  report += `----------------------------\n`;
  return report;
}

module.exports = formatOrderReport;

Setelah refactoring, jalankan lagi jest. Jika semua tes hijau, berarti refactoring Anda tidak mengubah perilaku yang ada. Jika ada tes yang gagal, berarti Anda secara tidak sengaja mengubah output. Anda bisa memeriksa perbedaan di laporan Jest atau di file .snap untuk melihat apa yang berubah.

Dengan cara ini, kita bisa merefaktor bagian demi bagian, sambil selalu memiliki jaring pengaman yang memastikan kita tidak merusak fungsionalitas yang sudah bekerja.

6. Tips dan Best Practices untuk Golden Master Testing

Untuk memaksimalkan efektivitas Golden Master Testing, perhatikan tips berikut:

7. Kelebihan dan Kekurangan Golden Master Testing

Seperti setiap alat, GMT memiliki kekuatan dan kelemahan:

Kelebihan:

Kekurangan: