Menyimpan data fixture hirarkis berskala besar—seperti kategori e-commerce berkedalaman tinggi, pohon organisasi, atau thread komentar bertingkat—dalam bentuk file JSON statis menimbulkan beban teknis. File berukuran puluhan megabite menggelembungkan riwayat Git, memicu konflik merge saat skema berubah, dan memperlambat proses bootstrap lingkungan lokal maupun pipeline Continuous Integration (CI).

Pendekatan prosedural menggunakan Lindenmayer system (L-system) menawarkan alternatif efisien. Dengan mendefinisikan sejumlah kecil aturan rewriting formal, developer dapat menghasilkan struktur pohon kompleks secara deterministik atau stokastik saat runtime pengujian berjalan, menjaga ukuran repositori tetap minimal.

Konsep Dasar L-System untuk Representasi Data Pohon

L-system adalah sistem penulisan ulang formal (formal rewriting system) yang diperkenalkan oleh biolog Aristid Lindenmayer pada tahun 1968. Pada implementasi grafis atau visualisasi botani, L-system umum digunakan bersama turtle graphics. Untuk kebutuhan pemodelan data rekayasa perangkat lunak, komponen L-system didefinisikan sebagai tuple G = (V, ω, P):

  • Alphabet (V): Simbol token yang valid. Untuk struktur hirarkis, kita menggunakan token entitas (misal N untuk Node) dan token percabangan ([ untuk membuka sub-pohon/child context, ] untuk menutup context).
  • Axiom (ω): String inisial yang menjadi titik awal ekspansi, misalnya N.
  • Production Rules (P): Kumpulan aturan pemetaan simbol ke string baru.

Pada L-system deterministik, satu simbol selalu memetakan ke ekspansi yang sama. Untuk variasi data pengujian (agar kedalaman dan lebar pohon tidak seragam), kita menerapkan L-system stokastik. Pada varian ini, sebuah simbol memiliki beberapa kandidat aturan dengan bobot probabilitas tertentu:

Axiom: N
Rules (Stokastik):
  N -> N[N]     (probabilitas 0.4: bercabang 1 anak)
  N -> N[N N]   (probabilitas 0.4: bercabang 2 anak)
  N -> N        (probabilitas 0.2: daun terminasi)

Konversi String L-System Menjadi Objek Bersarang

Hasil ekspansi rekursif dari aturan di atas berupa string seperti N[N[N] N]. String ini tidak bisa langsung digunakan oleh aplikasi pengujian sebelum diparsing ke struktur data nested object. Parser berbasis stack membaca string tersebut secara sekuensial:

  • Simbol N: Membuat entitas baru (misalnya node data kategori), memasukkannya ke dalam daftar children dari node induk aktif di stack.
  • Simbol [: Mendorong node terakhir yang dibuat ke dalam stack sebagai konteks parent aktif.
  • Simbol ]: Mengeluarkan konteks parent saat ini dari stack (pop), mengembalikan pointer ke parent sebelumnya.

Implementasi CLI Generator dengan Node.js Runtime

Implementasi berikut menggunakan JavaScript modern berbasis Node.js standard library tanpa dependensi pihak ketiga. Skrip menerima argumen kedalaman iterasi (depth) dan seed pseudorandom opsional untuk kebutuhan determinisme dalam CI.

#!/usr/bin/env node
import { writeFileSync } from 'node:fs';

function parseArgs() {
  const args = process.argv.slice(2);
  const config = { iterations: 3, output: null, stochastic: true };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--iterations' || args[i] === '-i') config.iterations = parseInt(args[++i], 10);
    if (args[i] === '--output' || args[i] === '-o') config.output = args[++i];
  }
  return config;
}

// Generator L-system
function expandLSystem(axiom, iterations) {
  let current = axiom;
  for (let i = 0; i < iterations; i++) {
    let next = '';
    for (const char of current) {
      if (char === 'N') {
        const rand = Math.random();
        if (rand < 0.4) next += 'N[N]';
        else if (rand < 0.8) next += 'N[N N]';
        else next += 'N';
      } else {
        next += char;
      }
    }
    current = next;
  }
  return current;
}

// Parser token ke struktur objek hirarkis
function buildTree(tokens) {
  const root = { id: 'root', name: 'Root', children: [] };
  const stack = [root];
  let lastCreated = null;
  let idCounter = 1;

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i];
    if (token === 'N') {
      const node = {
        id: `node-${idCounter}`,
        name: `Category-${idCounter}`,
        depth: stack.length - 1,
        children: []
      };
      idCounter++;
      stack[stack.length - 1].children.push(node);
      lastCreated = node;
    } else if (token === '[') {
      if (lastCreated) {
        stack.push(lastCreated);
      }
    } else if (token === ']') {
      if (stack.length > 1) {
        stack.pop();
      }
    }
  }
  return root.children;
}

function run() {
  const { iterations, output } = parseArgs();
  const lSystemString = expandLSystem('N', iterations);
  const treeData = buildTree(lSystemString);
  const json = JSON.stringify(treeData, null, 2);

  if (output) {
    writeFileSync(output, json, 'utf-8');
    process.stdout.write(`Fixture written to ${output}\n`);
  } else {
    process.stdout.write(json + '\n');
  }
}

run();
Catatan: Skrip di atas menggunakan Math.random() untuk simplisitas. Untuk lingkungan pengujian yang membutuhkan determinisme ketat (reproducible test failures), ganti dengan algoritma Linear Congruential Generator (LCG) atau seeded PRNG sederhana.

Integrasi pada Script Seeding dan Pipeline CI

Generator ini dapat dihubungkan langsung ke pipeline pengujian menggunakan pipe Unix atau script seeding database tanpa harus commit artifact JSON ke repositori.

Eksekusi Pipeline CLI

Jalankan generator secara inline sebelum suite integrasi dieksekusi:

# Menghasilkan pohon 4 level dan menyimpannya sebagai temporary fixture
node ./scripts/tree-gen.mjs -i 4 -o ./tests/fixtures/category-tree.json

# Atau langsung pipe ke database seeder melalui STDIN
node ./scripts/tree-gen.mjs -i 5 | node ./scripts/db-seed-categories.mjs

Evaluasi Teknis: Keuntungan dan Trade-off

Penggunaan L-system untuk fixture data hirarkis menghadirkan efisiensi teknis yang terukur dibanding pendekatan fixture statis tradisional:

  • Efisiensi Repositori: Mengganti file JSON fixture puluhan megabite dengan generator deklaratif berukuran kurang dari 3 KB. Git tree terbebas dari churn commit data dummy.
  • Kontrol Kompleksitas: Nilai iterations memetakan secara langsung ke eksponensial kedalaman pohon. Developer dapat memvalidasi performa algoritma rekursif aplikasi (seperti BFS traversal atau CTE queries pada SQL) di berbagai skala ukuran dengan hanya mengubah satu parameter integer.
  • Risiko Memory Explosion: L-system rentan terhadap pertumbuhan string eksponensial ($O(b^d)$ dengan $b$ sebagai branching factor rata-rata dan $d$ sebagai jumlah iterasi). Menetapkan guard clause batas iterasi maksimal sangat disarankan agar memory runtime tidak mengalami OOM (Out Of Memory).
  • Deterministik vs Flaky Test: Penggunaan aturan stokastik tanpa seed statis pada pipeline CI dapat menyebabkan tes mengalami flakiness jika ada node boundary yang gagal diproses aplikasi. Pastikan seed tetap dipin pada level environment test (misalnya SEED=12345).