Membuat Sistem Auto-Test (TDD): Biarkan AI Menulis Unit Test Sebelum Menulis Kode Fitur
Bayangkan Anda mempekerjakan seorang insinyur magang yang luar biasa cerdas, membaca ribuan dokumentasi teknis dalam hitungan detik, mengetik dengan kecepatan 500 kata per menit, namun mengidap sedikit kecenderungan patologis: ia terlalu percaya diri dan tidak pernah memeriksa kembali apakah kran air yang ia pasang tersambung ke pipa pembuangan atau saluran gas.
Itulah representasi paling akurat dari Large Language Models (LLM) saat ini ketika diminta menulis kode.
Jika Anda menyuruh AI langsung menulis implementasi fitur yang rumit, apa yang Anda dapatkan sering kali adalah "ilusi fungsional". Kodenya tampak rapi, sintaksisnya valid, penamaan variabelnya elegan—namun begitu disentuh oleh beban produksi, seluruh pondasi rapuh itu runtuh karena asumsi implisit yang salah, penanganan edge cases yang bolong, dan mutasi state yang liar. Istilah populernya adalah vibe coding: program berjalan di komputer lokal karena Anda kebetulan hanya mencoba skenario ideal.
Untuk mengendalikan entitas stokastik (berbasis probabilitas) seperti AI, kita tidak bisa melawannya dengan instruksi verbal yang semakin panjang. Kita butuh jangkar deterministik.
Jangkar tersebut sudah ditemukan oleh dunia rekayasa perangkat lunak sejak dua dekade lalu: Test-Driven Development (TDD).
Namun dengan keterlibatan model penalaran mutakhir, aturan mainnya berubah. Pola paling efektif bukan lagi manusia menulis tes lalu AI menulis kode, melainkan memaksa AI menulis unit test yang kejam dan komprehensif terlebih dahulu, mengunci batas-batas invarian sistem, baru kemudian membiarkannya menulis kode implementasi hingga seluruh tes tersebut lolos.
Ilusi Kecepatan dan Bahaya "Vibe Coding"
Ada perbedaan fundamental antara bagaimana manusia dan mesin membuat kesalahan. Ketika programmer manusia lelah, mereka membuat kesalahan sintaks atau off-by-one error yang kentara. Namun ketika LLM berhalusinasi, ia menciptakan halusinasi yang meyakinkan: memanggil fungsi pustaka yang tidak pernah ada namun terdengar masuk akal, atau membiarkan race condition terjadi di balik abstraksi async/await yang tampak mulus.
Pendekatan Naif:
Prompt Fitur ───> LLM Buat Kode ───> Run Manual(Error) ───> Prompt Debug Ulang(Kode Tambah Kusut)
Pendekatan AI-TDD:
Prompt Spek ───> LLM Buat Test(PyTest) ───> Kunci Kontrak ───> LLM Buat Kode ───> Run Test(Pass)Ketika Anda meminta AI langsung menyusun berkas payment_processor.py, AI akan memilih jalur probabilistik termudah—jalur di mana saldo selalu cukup, jaringan tidak pernah putus, dan input pengguna selalu bersih.
Sebaliknya, ketika Anda membalik urutannya dan memerintahkan AI untuk bertindak sebagai adversarial QA engineer yang bertugas membongkar kelemahan sistem melalui test_payment_processor.py, AI dipaksa mengeksplorasi cabang probabilitas ekstrem: apa yang terjadi jika saldo bernilai -0.0001? Apa yang terjadi jika database melempar timeout saat pemotongan kredit separuh jalan?
Dengan memisahkan fase penetapan spesifikasi (Unit Test) dari fase implementasi, Anda mengubah AI dari juru ketik yang ceroboh menjadi arsitek yang terikat kontrak hukum logika.
Komparasi Paradigma Rekayasa Kode
Untuk melihat mengapa metode ini superior bagi tim engineering modern, mari bandingkan tiga pendekatan umum dalam pengembangan perangkat lunak:
| Parameter | Vibe Coding (Prompt & Pray) | Conventional TDD (Manual) | AI-Assisted TDD (Invariants First) |
|---|---|---|---|
| Penyusun Test | Tidak ada / Ditulis belakangan | Manusia (Lambat, Teliti) | AI dengan panduan spesifikasi manusia |
| Waktu Siklus (Loop) | Cepat di awal, hancur di maintenance | Lambat di awal, stabil selamanya | Cepat di awal, stabil di produksi |
| Cakupan Edge Case | Sangat rendah (< 30%) | Tergantung imajinasi engineer | Sangat tinggi (> 90% via variasi prompt) |
| Biaya Token / Iterasi | Boros (debug loop tanpa henti) | Nol (manual) | Terukur & efisien via skrip otomatis |
| Tingkat Halusinasi | Tinggi (kode & asumsi fiktif) | Rendah | Terisolasi & langsung tertangkap di level runner |
| Peran Manusia | Debugger kode berantakan | Juru ketik & perancang logika | Specifier, kurator logika, & pengambil keputusan |
Anatomi Praktik: Membangun Sistem Billing Multi-Tenant
Mari kita bedah alur kerja ini ke dalam skenario nyata. Kita akan membangun modul kuota transaksi multi-tenant untuk layanan SaaS menggunakan Python dan pytest.
Aturan bisnis kita (Invarian):
- Setiap organisasi memiliki alokasi kredit bulanan.
- Pengurangan kredit harus bersifat atomik; tidak boleh menghasilkan saldo negatif dalam kondisi konkuren.
- Transaksi di atas sisa saldo harus menolak eksekusi dengan eksepsi
InsufficientCreditError. - Pengembalian kredit (refund) tidak boleh melebihi batas kuota maksimum yang ditetapkan.
Langkah 1: Merumuskan Prompt Kontrak untuk Unit Test
Alih-alih meminta AI menulis modul billing, kita berikan prompt khusus untuk memproduksi skenario pengujian yang komprehensif.
[PROMPT KE AI]
Bertindaklah sebagai Senior QA Architect & Python Security Engineer.
Saya ingin kamu membuat berkas test_billing_engine.py menggunakan PyTest.
Jangan tulis kode implementasinya sekarang. Hanya tulis unit test dan integration test.
Spesifikasi Modul `BillingEngine`:
- Method `deduct_credits(org_id: str, amount: Decimal) -> TransactionResult`
- Method `refund_credits(org_id: str, amount: Decimal, original_tx_id: str) -> TransactionResult`
- Method `get_balance(org_id: str) -> Decimal`
Wajib mencakup pengujian:
1. Nominal pecahan desimal presisi tinggi(hindari bug float rounding).
2. Upaya pengurangan saldo melampaui limit(harus raise InsufficientCreditError).
3. Validasi parameter input(nilai negatif, string kosong, tipe data salah).
4. Skenario idempotensi pada refund.Langkah 2: Hasil Test Suite (Invarian yang Terkunci)
AI akan menghasilkan berkas test_billing_engine.py yang ketat. Di sinilah letak kekuatannya: tes ini menjadi pagar kawat berduri yang tidak bisa ditembus oleh halusinasi implementasi nanti.
# test_billing_engine.py
import pytest
from decimal import Decimal
from unittest.mock import Mock
# Asumsikan modul belum ada, kita mendefinisikan interface yang diharapkan
from billing_engine import BillingEngine, InsufficientCreditError, InvalidAmountError
@pytest.fixture
def mock_storage():
"""Mock storage layer yang menyimpan saldo in-memory untuk pengujian."""
storage = {}
return storage
@pytest.fixture
def billing_service(mock_storage):
return BillingEngine(storage_adapter=mock_storage, max_credit_cap=Decimal("10000.00"))
def test_deduct_credits_success(billing_service, mock_storage):
org_id = "org_enterprise_99"
mock_storage[org_id] = Decimal("500.00")
result = billing_service.deduct_credits(org_id, Decimal("150.50"))
assert result.success is True
assert result.remaining_balance == Decimal("349.50")
assert billing_service.get_balance(org_id) == Decimal("349.50")
def test_deduct_credits_insufficient_funds(billing_service, mock_storage):
org_id = "org_startup_01"
mock_storage[org_id] = Decimal("50.00")
with pytest.raises(InsufficientCreditError) as exc_info:
billing_service.deduct_credits(org_id, Decimal("50.01"))
assert "Saldo tidak mencukupi" in str(exc_info.value)
# Pastikan saldo tidak termutasi saat gagal
assert billing_service.get_balance(org_id) == Decimal("50.00")
@pytest.mark.parametrize("invalid_amount", [
Decimal("-10.00"),
Decimal("0.00"),
Decimal("-0.0001"),
])
def test_deduct_credits_invalid_amounts(billing_service, invalid_amount):
with pytest.raises(InvalidAmountError):
billing_service.deduct_credits("org_any", invalid_amount)
def test_refund_cannot_exceed_max_cap(billing_service, mock_storage):
org_id = "org_whale_07"
mock_storage[org_id] = Decimal("9950.00")
# Cap maksimum adalah 10,000.00. Refund 100.00 harus gagal atau ditolak.
with pytest.raises(ValueError) as exc_info:
billing_service.refund_credits(org_id, Decimal("100.00"), original_tx_id="tx_123")
assert "Melebihi batas alokasi maksimum" in str(exc_info.value)Jika kita menjalankan pytest test_billing_engine.py saat ini di terminal, hasilnya adalah Merah (Fail) karena modul billing_engine.py bahkan belum dibuat. Ini adalah esensi TDD: Red Phase.
$ pytest test_billing_engine.py
ModuleNotFoundError: No module named 'billing_engine'
FAILED test_billing_engine.py - 4 failed in 0.05sLangkah 3: Menginstruksikan AI Menulis Kode Implementasi
Kini, tugas AI menjadi sangat sederhana dan terfokus: Buat tes di atas menjadi hijau (Green Phase).
Kita masukkan berkas test_billing_engine.py ke dalam context prompt, lalu minta AI menyusun billing_engine.py. AI tidak lagi memiliki ruang untuk berimajinasi liar atau menebak-nebak format output, karena fungsi dan eksepsi sudah terdefinisi secara presisi.
# billing_engine.py
from decimal import Decimal
from dataclasses import dataclass
from typing import Dict, Any
class InsufficientCreditError(Exception):
pass
class InvalidAmountError(Exception):
pass
@dataclass(frozen=True)
class TransactionResult:
success: bool
remaining_balance: Decimal
transaction_id: str
class BillingEngine:
def __init__(self, storage_adapter: Dict[str, Decimal], max_credit_cap: Decimal):
self.storage = storage_adapter
self.max_credit_cap = max_credit_cap
def get_balance(self, org_id: str) -> Decimal:
return self.storage.get(org_id, Decimal("0.00"))
def deduct_credits(self, org_id: str, amount: Decimal) -> TransactionResult:
if amount <= Decimal("0.00"):
raise InvalidAmountError("Jumlah potongan harus lebih besar dari nol.")
current_balance = self.get_balance(org_id)
if current_balance < amount:
raise InsufficientCreditError(f"Saldo tidak mencukupi. Sisa: {current_balance}, Diminta: {amount}")
new_balance = current_balance - amount
self.storage[org_id] = new_balance
return TransactionResult(
success=True,
remaining_balance=new_balance,
transaction_id=f"tx_deduct_{org_id}_{len(self.storage)}"
)
def refund_credits(self, org_id: str, amount: Decimal, original_tx_id: str) -> TransactionResult:
if amount <= Decimal("0.00"):
raise InvalidAmountError("Jumlah refund harus lebih besar dari nol.")
current_balance = self.get_balance(org_id)
if (current_balance + amount) > self.max_credit_cap:
raise ValueError("Melebihi batas alokasi maksimum yang diizinkan.")
new_balance = current_balance + amount
self.storage[org_id] = new_balance
return TransactionResult(
success=True,
remaining_balance=new_balance,
transaction_id=f"tx_refund_{original_tx_id}"
)Jalankan kembali PyTest:
$ pytest test_billing_engine.py
============================== 4 passed in 0.02s ==============================Dalam waktu kurang dari dua menit, Anda memiliki kode produksi yang bersih, minim sampah boilerplate, dan yang terpenting: perilakunya terbukti benar di hadapan test runner.
Membangun "Self-Healing Loop" Menggunakan AiStudio.id
Pendekatan di atas menjadi berlipat ganda kekuatannya ketika kita mengotomatisasikannya ke dalam sebuah closed-loop agent. Alih-alih menyalin kode secara manual dari antarmuka web, kita bisa membuat skrip lokal sederhana yang mengirimkan tes ke AI, mengeksekusi PyTest di mesin lokal, dan jika terjadi kegagalan, mengirimkan error traceback kembali ke AI untuk diperbaiki secara mandiri.
Di sinilah peran infrastruktur API menjadi krusial. Dalam loop otomatis seperti ini, latensi dan reliabilitas API menentukan apakah proses refactoring memakan waktu 10 detik atau 2 menit.
Menggunakan gateway seperti AiStudio.id API Gateway memberikan keuntungan arsitektural yang signifikan: Anda dapat menggunakan satu unified endpoint yang kompatibel dengan format SDK standar (seperti OpenAI SDK), namun memiliki fleksibilitas penuh untuk mengganti engine penalaran di balik layar.
┌─────────────────────────────────────────────────────────────┐
│ Local Developer Environment │
│ │
│ ┌─────────────┐ Prompt Spek ┌─────────────┐ │
│ │ Spec / Docs │ ──────────────────────> │ Auto-Agent │ │
│ └─────────────┘ │ Python CLI │ │
│ ▲ └──────┬──────┘ │
│ │ │ │
│ │ Request │ Unified │
│ │ Payload│ API Call │
└──────────┼───────────────────────────────────────┼──────────┘
│ ▼
│ ┌─────────────────────────┐
│ │ AiStudio.id Gateway │
│ │ (Low Latency / Router) │
│ └──────────────┬──────────┘
│ │
│ ┌─────────────────┴─────────────────┐
│ ▼ ▼
│ ┌───────────────────────┐ ┌───────────────────────┐
│ │ Anthropic Claude 3.7 │ │ Google Gemini 2.0 │
│ │ (Arsitek Unit Test) │ │ (Fast Code Patching) │
│ └──────────┬────────────┘ └───────────┬───────────┘
│ │ │
│ └─────────────────┬──────────────────┘
│ │ Generated Test / Code
│ ▼
┌──────────┴──────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Local Test Execution: `pytest --json-report` │ │
│ └──────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ ▼ ▼ │
│ [ Exit Code == 0 ] [ Exit Code != 0 ] │
│ │ │ │
│ STATUS: SUCCESS Kirim Traceback │
│ (Commit & Refactor) Kembali ke Gateway │
│ (Loop Maksimal 3x) │
└─────────────────────────────────────────────────────────────┘Misalnya, Anda dapat menugaskan model dengan kemampuan penalaran tinggi seperti Claude 3.7 Sonnet atau DeepSeek-R1 untuk menyusun rancangan pengujian awal, lalu beralih ke Gemini 2.0 Flash untuk siklus perbaikan kode yang membutuhkan kecepatan inferensi kilat dengan biaya token yang sangat rendah.
Berikut contoh implementasi script autonomous test runner sederhana menggunakan Python:
# auto_tdd_runner.py
import subprocess
import os
from openai import OpenAI
# Menggunakan endpoint AiStudio.id API Gateway
client = OpenAI(
api_key=os.environ.get("AISTUDIO_API_KEY"),
base_url="https://api.aistudio.id/v1"
)
def run_pytest():
"""Menjalankan pytest dan menangkap stdout/stderr serta exit code."""
result = subprocess.run(
["pytest", "test_billing_engine.py", "-v", "--tb=short"],
capture_output=True,
text=True
)
return result.returncode, result.stdout
def heal_code_with_ai(test_code: str, broken_code: str, error_log: str) -> str:
"""Mengirim traceback error ke AiStudio.id untuk perbaikan otomatis."""
prompt = f"""
Kamu adalah software engineer handal. Unit test berikut ini GAGAL.
=== TEST CODE ===
{test_code}
=== CURRENT IMPLEMENTATION ===
{broken_code}
=== PYTEST TRACEBACK ERROR ===
{error_log}
Perbaiki `billing_engine.py` agar semua pengujian di atas lulus 100%.
Keluarkan HANYA kode Python murni tanpa markdown, tanpa backticks, tanpa penjelasan.
"""
response = client.chat.completions.create(
model="claude-3-7-sonnet-20250219", # Tersedia via routing AiStudio.id
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
return response.choices[0].message.content.strip()
def main():
max_retries = 3
attempt = 0
with open("test_billing_engine.py", "r") as f:
test_code = f.read()
while attempt < max_retries:
attempt += 1
print(f"[*] Menjalankan Test Suite(Percobaan ke-{attempt})...")
exit_code, output = run_pytest()
if exit_code == 0:
print("[+] SUKSES: Semua unit test berhasil lolos!")
break
print(f"[!] Test gagal. Mengirimkan output kegagalan ke AiStudio.id Gateway...")
current_code = ""
if os.path.exists("billing_engine.py"):
with open("billing_engine.py", "r") as f:
current_code = f.read()
fixed_code = heal_code_with_ai(test_code, current_code, output)
with open("billing_engine.py", "w") as f:
f.write(fixed_code)
print("[*] Perbaikan kode telah diterapkan dari AI. Menjalankan verifikasi ulang...")
if __name__ == "__main__":
main()Skrip di atas mendemonstrasikan bagaimana siklus perbaikan kode tidak lagi membutuhkan intervensi manusia untuk hal-hal sepele seperti kesalahan tipe data atau salah penamaan variabel. Selama definisi tes dipegang teguh, AI akan berputar memperbaiki dirinya sendiri hingga kontrak logika terpenuhi.
Prinsip Menjaga Invarian Tetap Kebal Halusinasi
Agar strategi AI-TDD ini tidak justru menjadi bumerang—di mana AI membuat unit test yang sengaja "dibuat mudah" agar selalu lolos—ada beberapa aturan taktik yang wajib Anda terapkan dalam prompt engineering:
1. Pisahkan Sesi Konteks Antara Tester dan Coder
Jangan pernah meminta AI menulis test dan kode implementasi dalam satu pesan percakapan yang sama. Jika Anda melakukannya, AI akan mencocokkan tes tersebut dengan keterbatasan kode yang ia rancang sendiri di kepalanya. Selalu minta test di sesi/file terpisah, pastikan tes tersebut gagal, lalu berikan test itu ke sesi baru untuk dibuatkan implementasinya.2. Gunakan Property-Based Testing dengan Hypothesis
Selain pengujian nilai statis menggunakan @pytest.mark.parametrize, manfaatkan pustaka seperti Hypothesis. Minta AI menyusun pengujian berbasis properti matematika:
from hypothesis import given, strategies as st
from decimal import Decimal
@given(st.decimals(min_value=Decimal("0.01"), max_value=Decimal("1000.00")))
def test_balance_always_decreases_by_exact_amount(billing_service, mock_storage, amount):
org_id = "org_fuzz"
mock_storage[org_id] = Decimal("2000.00")
initial = billing_service.get_balance(org_id)
billing_service.deduct_credits(org_id, amount)
final = billing_service.get_balance(org_id)
assert initial - final == amountDengan strategi ini, ribuan kombinasi angka acak akan dibombardirkan ke kode buatan AI. Jika ada celah floating-point atau konversi tipe data yang janggal, PyTest akan langsung menemukannya.
3. Terapkan Prinsip "Negative-First"
AI secara alami bias terhadap jalur sukses (happy path). Anda harus secara eksplisit mendikte AI untuk mendedikasikan minimal 60% dari seluruh fungsi tes untuk skenario kegagalan: network timeout, payload injection, nilai null, karakter non-UTF8, dan duplikasi ID.Pergeseran Peran Software Engineer
Ada ketakutan yang sering terdengar bahwa kehadiran model penalaran generatif akan melenyapkan kebutuhan akan pemahaman mendalam tentang pemrograman. Pengalaman di lapangan menunjukkan hal yang sebaliknya.
Ketika kecepatan mengetik kode didevaluasi menjadi komoditas murah berbiaya nol, nilai seorang engineer bergeser ke dua hal:
- Ketajaman perumusan spesifikasi (Specification Rigor): Kemampuan mendefinisikan apa yang sebenarnya ingin dibangun tanpa ada ambiguitas logika.
- Keahlian audit & verifikasi (Verification Expertise): Kemampuan membedakan sistem yang benar-benar kokoh dari sistem yang sekadar "terlihat berjalan".
Metode AI-TDD menempatkan manusia tepat di kursi pengemudi yang sesungguhnya. Anda tidak lagi menghabiskan 8 jam sehari berkutat dengan sintaks loop atau menyelaraskan format JSON. Anda bertindak sebagai hakim yang menentukan aturan hukum sistem, membiarkan model AI mengeksplorasi ribuan baris implementasi di bawah pengawasan ketat test suite yang Anda kendalikan.
Dengan memanfaatkan perkakas pengujian yang matang seperti PyTest, diintegrasikan dengan jalur akses multi-model yang cepat dan terukur melalui gateway seperti AiStudio.id, Anda tidak sekadar memproduksi kode lebih cepat. Anda membangun benteng perangkat lunak yang tahan banting—di mana setiap baris kode yang ditulis oleh kecerdasan buatan memiliki sertifikat pembuktian yang tak terbantahkan.
Catatan Penulis

> Sandra menulis seputar rekayasa prompt, efisiensi arsitektur AI, dan produk digital di AiStudio.id.