Lompat ke konten utama
vourdev
Kembali ke Blog

Dev Notes

Cara Saya Pakai AI Buat Nemu Edge Cases dan Bikin Unit Test Otomatis di TypeScript

Workflow harian memanfaatkan LLM untuk stress-test logika bisnis TypeScript, nemu boundary errors yang sering terlewat, dan bikin unit test otomatis.

Gambar Cover Cara Saya Pakai AI Buat Nemu Edge Cases dan Bikin Unit Test Otomatis di TypeScript
Ditulis olehvourdev12 menit baca

Jujur aja, menulis unit test itu sering terasa membosankan. Banyak engineer—termasuk saya dulu—baru bikin test pas fitur udah keburu deployed ke production dan nemu bug konyol. Masalah paling sering bukan karena kita nggak paham syntax Vitest atau Jest, tapi karena otak manusia punya bias bawaan: kita cenderung cuma ngetest happy path.

Pas nulis kode checkout pembayaran atau kalkulasi diskon bertingkat, pikiran kita fokus ke logika utama: "User beli barang X, dapet diskon Y, bayar Z." Tapi gimana kalau stok barang habis persis di milisekon yang sama? Gimana kalau nominal diskon menghasilkan angka desimal bertingkat yang bikin floating point precision error? Atau gimana kalau user kirim payload berupa array kosong di dalam objek yang ter-nested?

Di sinilah AI seperti Claude atau GPT-4 masuk ke workflow harian saya. Saya nggak minta AI buat "nulisin semua kode aplikasi". Itu pendekatan malas yang sering menghasilkan slop code. Sebaliknya, saya pakai AI sebagai adversarial reviewer—seorang partner pair-programming yang bertugas nyari celah hancurnya logika bisnis yang baru saya tulis, lalu bikin unit test ketat buat guardrail sistem tersebut.

Di artikel ini, saya bakal bongkar workflow konkret, system prompt yang saya pakai, dan contoh kode nyata berbasis TypeScript dan Vitest.


1. Realita Pahit: Kenapa Manual Edge Case Hunting Itu Cepat Bikin Burnout

Bayangkan kita punya modul perhitungan biaya sewa properti. Sekilas kodenya sederhana: hitung durasi hari, kalikan tarif harian, terapkan diskon mingguan jika ada, lalu tambahkan pajak.

Coba perhatikan fungsi TypeScript di bawah ini:

typescript
// booking.service.ts

export interface BookingRequest {
  startDate: Date;
  endDate: Date;
  dailyRate: number;
  weeklyDiscountPercent: number;
  taxRate: number;
}

export interface BookingResult {
  totalDays: number;
  subtotal: number;
  discountAmount: number;
  taxAmount: number;
  grandTotal: number;
}

export function calculateBookingTotal(req: BookingRequest): BookingResult {
  const diffTime = req.endDate.getTime() - req.startDate.getTime();
  const totalDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

  let discountPercent = 0;
  if (totalDays >= 7) {
    discountPercent = req.weeklyDiscountPercent;
  }

  const rawSubtotal = totalDays * req.dailyRate;
  const discountAmount = rawSubtotal * (discountPercent / 100);
  const taxableAmount = rawSubtotal - discountAmount;
  const taxAmount = taxableAmount * (req.taxRate / 100);
  const grandTotal = taxableAmount + taxAmount;

  return {
    totalDays,
    subtotal: rawSubtotal,
    discountAmount,
    taxAmount,
    grandTotal,
  };
}

Sekilas kode di atas aman, kan? Dites pakai manual testing via Postman juga return angkanya kelihatan benar. Tapi di lingkungan production, fungsi ini adalah bom waktu. Ada minimal 6 edge cases fatal di fungsi kecil ini:

  1. Daylight Saving Time (DST) & Timezone Shifts: Math.ceil(diffTime / 86400000) bisa berantakan kalau rentang tanggal melewati pergantian jam DST (durasi sehari bisa 23 atau 25 jam).
  2. Reverse Dates: endDate lebih awal dari startDate menghasilkan totalDays negatif.
  3. Zero / Negative Rates: dailyRate minus atau nol.
  4. Floating Point Rounding Error: Hasil perkalian desimal untuk transaksi keuangan tidak di-round (misal 100.00000000000001).
  5. Discount Overflow: weeklyDiscountPercent diisi angka > 100 atau < 0.
  6. Invalid Date Objects: Passing new Date('invalid-string') yang menghasilkan NaN pada .getTime().

Kalau harus mikirin puluhan skenario seperti ini untuk ratusan fungsi di codebase, waktu habis cuma buat melamun nyari bug. AI sangat bagus untuk tugas ini karena LLM dilatih dari jutaan post-mortem bug dan repositori open-source.


2. Dynamic Workflow: Bagaimana Saya Menggunakan LLM di Editor

Workflow harian saya tidak melibatkan copy-paste manual ke UI ChatGPT tiap 5 menit. Saya menggunakan ekstensi AI agent (atau CLI tools) yang langsung mengakses file TypeScript di workspace.

Prosesnya terdiri dari 3 tahap utama:

code
+-----------------------+     +-----------------------+     +-----------------------+
| 1. Business Logic     | --> | 2. Adversarial        | --> | 3. Automated Test     |
|    Implementation     |     |    Edge Case Attack   |     |    Generation & Fix   |
+-----------------------+     +-----------------------+     +-----------------------+

Langkah 1: Berikan Prompt "Adversarial Tester"

Jangan minta AI sekadar "Buatkan unit test untuk fungsi ini". Kalau prompt-nya generic, hasil test-nya cuma bakal ngecek happy path yang standar. Pakai teknik Role-Based Stress Testing.

Berikut prompt template yang biasa saya pakai:

Prompt Prompting Master Template:
"Bertindaklah sebagai Senior QA Automation Engineer & Security Researcher. Analisis fungsi TypeScript berikut. Tugasmu BUKAN nulis test biasa, tapi mencari cara untuk MERUSAK (break) fungsi ini. Daftarkan minimal 8 edge cases paling jahat, termasuk boundary value, invalid input, floating point issues, dan timezone/date anomalies. Jangan tulis kode dulu, paparkan dulu daftar edge cases-nya."

3. Hasil Bedah Edge Cases dari LLM

Ketika fungsi calculateBookingTotal di atas saya umpankan ke LLM dengan prompt tersebut, AI langsung mengembalikan laporan seperti ini:

markdown
1. Invalid Date (NaN Check):
   - Input: startDate = new Date('invalid')
   - Ekspektasi: Throw Error secara eksplisit.

2. Reverse Date (endDate < startDate):
   - Input: startDate = 2026-03-10, endDate = 2026-03-01
   - Ekspektasi: Throw RangeError ("endDate must be after startDate").

3. Single-day DST Transition (23 Hours Day):
   - Input: Rentang tanggal saat perubahan jam Daylight Saving.
   - Ekspektasi: totalDays tetap dihitung 1 hari penuh, bukan 0 atau 2 hari.

4. Floating Point Precision Issue:
   - Input: dailyRate = 19.99, taxRate = 8.875, totalDays = 3
   - Ekspektasi: Nilai dikembalikan presisi 2 desimal (dibulatkan dengan benar), tanpa trailing .000000000004.

5. Negative / Abnormal Discounts:
   - Input: weeklyDiscountPercent = 150% atau -20%
   - Ekspektasi: Validation error / sanitasi boundary input.

4. Bikin Automated Test Suite Pakai Vitest

Setelah daftar edge case disepakati, baru kita minta LLM menembakkan kode unit test lengkap menggunakan Vitest (atau Jest).

Berikut unit test yang dihasilkan oleh AI untuk menguji boundary dan error condition fungsi tersebut:

typescript
// booking.service.spec.ts
import { describe, it, expect } from 'vitest';
import { calculateBookingTotal, BookingRequest } from './booking.service';

describe('calculateBookingTotal - Edge Cases & Stress Tests', () => {
  it('harus melempar error jika startDate atau endDate bernilai Invalid Date', () => {
    const invalidReq: BookingRequest = {
      startDate: new Date('invalid-date-string'),
      endDate: new Date('2026-03-10'),
      dailyRate: 100,
      weeklyDiscountPercent: 10,
      taxRate: 10,
    };

    expect(() => calculateBookingTotal(invalidReq)).toThrowError(/invalid date/i);
  });

  it('harus melempar error jika endDate lebih awal dari startDate', () => {
    const reverseReq: BookingRequest = {
      startDate: new Date('2026-03-10'),
      endDate: new Date('2026-03-05'),
      dailyRate: 100,
      weeklyDiscountPercent: 10,
      taxRate: 10,
    };

    expect(() => calculateBookingTotal(reverseReq)).toThrowError(
      'endDate harus setelah startDate'
    );
  });

  it('harus menangani kalkulasi floating point desimal tanpa presisi aneh', () => {
    const floatReq: BookingRequest = {
      startDate: new Date('2026-03-01T00:00:00Z'),
      endDate: new Date('2026-03-04T00:00:00Z'), // 3 hari
      dailyRate: 19.99,
      weeklyDiscountPercent: 0,
      taxRate: 8.875,
    };

    const result = calculateBookingTotal(floatReq);

    // 3 * 19.99 = 59.97
    // Tax = 59.97 * 0.08875 = 5.3223375 -> dibulatkan jadi 5.32
    // Grand Total = 59.97 + 5.32 = 65.29
    expect(result.subtotal).toBe(59.97);
    expect(result.taxAmount).toBe(5.32);
    expect(result.grandTotal).toBe(65.29);
  });

  it('harus menolak persentase diskon di luar rentang 0 - 100', () => {
    const invalidDiscountReq: BookingRequest = {
      startDate: new Date('2026-03-01'),
      endDate: new Date('2026-03-10'),
      dailyRate: 100,
      weeklyDiscountPercent: 150, // Invalid discount
      taxRate: 10,
    };

    expect(() => calculateBookingTotal(invalidDiscountReq)).toThrowError(
      'weeklyDiscountPercent harus di antara 0 dan 100'
    );
  });

  it('harus menghitung jumlah hari dengan tepat meskipun melewati rentang jam DST', () => {
    // Contoh simulasi perbedaan waktu jam
    const start = new Date('2026-03-29T00:00:00Z');
    const end = new Date('2026-03-30T00:00:00Z');

    const req: BookingRequest = {
      startDate: start,
      endDate: end,
      dailyRate: 100,
      weeklyDiscountPercent: 0,
      taxRate: 0,
    };

    const result = calculateBookingTotal(req);
    expect(result.totalDays).toBe(1);
    expect(result.grandTotal).toBe(100);
  });
});

5. Refactoring Kode Berdasarkan Feedback Test

Saat kita me-run test suite di atas (npx vitest run), bisa dipastikan hampir semua test bakal fail karena fungsi awal kita belum menangani guards tersebut.

Sekarang tugas kita (bersama AI) adalah melakukan refactoring pada fungsi asli sampai semua test passing (Green phase pada TDD cycle).

Berikut kode booking.service.ts yang sudah diperbaiki:

typescript
// booking.service.ts (Refactored & Secure)

export interface BookingRequest {
  startDate: Date;
  endDate: Date;
  dailyRate: number;
  weeklyDiscountPercent: number;
  taxRate: number;
}

export interface BookingResult {
  totalDays: number;
  subtotal: number;
  discountAmount: number;
  taxAmount: number;
  grandTotal: number;
}

// Helper untuk pembulatan mata uang yang aman (2 desimal)
function roundCurrency(val: number): number {
  return Math.round((val + Number.EPSILON) * 100) / 100;
}

export function calculateBookingTotal(req: BookingRequest): BookingResult {
  // 1. Guard against invalid Date objects
  if (isNaN(req.startDate.getTime()) || isNaN(req.endDate.getTime())) {
    throw new Error('Invalid date provided');
  }

  // 2. Guard against reverse dates
  if (req.endDate <= req.startDate) {
    throw new Error('endDate harus setelah startDate');
  }

  // 3. Guard against invalid parameters
  if (req.dailyRate < 0) {
    throw new Error('dailyRate tidak boleh negatif');
  }

  if (req.weeklyDiscountPercent < 0 || req.weeklyDiscountPercent > 100) {
    throw new Error('weeklyDiscountPercent harus di antara 0 dan 100');
  }

  // 4. Calculate total days safely (UTC boundary math)
  const utcStart = Date.UTC(
    req.startDate.getFullYear(),
    req.startDate.getMonth(),
    req.startDate.getDate()
  );
  const utcEnd = Date.UTC(
    req.endDate.getFullYear(),
    req.endDate.getMonth(),
    req.endDate.getDate()
  );

  const totalDays = Math.round((utcEnd - utcStart) / (1000 * 60 * 60 * 24));

  // 5. Calculate financials with precision rounding
  let discountPercent = 0;
  if (totalDays >= 7) {
    discountPercent = req.weeklyDiscountPercent;
  }

  const rawSubtotal = roundCurrency(totalDays * req.dailyRate);
  const discountAmount = roundCurrency(rawSubtotal * (discountPercent / 100));
  const taxableAmount = roundCurrency(rawSubtotal - discountAmount);
  const taxAmount = roundCurrency(taxableAmount * (req.taxRate / 100));
  const grandTotal = roundCurrency(taxableAmount + taxAmount);

  return {
    totalDays,
    subtotal: rawSubtotal,
    discountAmount,
    taxAmount,
    grandTotal,
  };
}

Penjelasan Detail Perbaikan:

  • `isNaN(date.getTime())`: Mencegah runtime crash akibat Invalid Date yang lolos dari pengecekan tipe TypeScript.
  • `Date.UTC(...)`: Mengabaikan offset timezone lokal server/client sehingga durasi hari dihitung konsisten murni dari pergantian tanggal kalender.
  • `roundCurrency()` dengan `Number.EPSILON`: Mengatasi masalah klasik 0.1 + 0.2 !== 0.3 pada Floating Point IEEE 754 di JavaScript.

6. Tips & Gotchas Menggunakan AI dalam Unit Testing

Meskipun AI sangat membantu, ada beberapa pitfall yang wajib kamu waspadai:

Peringatan Paling Penting:
1. Jangan Percaya Assertion Halu: AI sering bikin mock data yang keliru di mana hasil ekspektasi (expect(...)) ditulis asal jadi agar test-nya selalu pass. Selalu hitung manual sample test secara acak.
2. Avoid Testing Implementation Details: Minta AI untuk fokus mengecek Output berdasarkan Input (Black-box testing), bukan mengecek fungsi internal/private dipanggil berapa kali (White-box testing over-mocking).
3. TDD Reverse Pattern: Cara paling ampuh adalah menulis test yang dihasilkan AI sebelum kamu memperbaiki kode aslimu. Pastikan test-nya FAIL dulu, baru jalankan refactor sampai PASS.

Kesimpulan & Action Items

Menggunakan AI bukan tentang memotong kompas demi malas berpikir. Bagi senior engineer, AI adalah multiplier yang mempercepat proses membosankan seperti identifikasi corner cases dan penulisan boilerplate code.

Langkah konkret yang bisa kamu coba hari ini:

  1. Ambil 1 fungsi paling kompleks di codebase timmu yang belum punya unit test.
  2. Jalankan prompt Adversarial Tester yang sudah kita bahas tadi di Claude/GPT-4.
  3. Salin unit test yang dihasilkan ke file spec baru pakai Vitest/Jest.
  4. Perbaiki guard validation pada logika bisnismu sampai seluruh test hijau.

Dengan workflow ini, kamu tidak hanya menghemat waktu jam-jaman penulisan test, tapi juga tidur nyenyak di malam hari tanpa khawatir kedatangan pings dari PagerDuty gara-gara bug sepele di production.

Butuh penyesuaian khusus untuk project Anda?

Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.

Mulai Project