Test narrative adalah cara menulis skenario uji dalam bentuk naratif yang ringkas, eksplisit, dan mudah dibaca manusia sebelum ia diterjemahkan menjadi test otomatis. Pendekatan ini berguna ketika regresi tidak lagi bisa dijelaskan hanya lewat unit test terpisah, misalnya pada alur checkout, sinkronisasi status lintas service, approval workflow, atau perubahan state yang dipicu event.

Jika tim Anda sering bertanya "sebenarnya perilaku yang ingin dipertahankan apa?" setiap kali ada bug lama muncul kembali, maka test narrative biasanya lebih membantu daripada sekadar menambah assertion acak. Narrative yang baik menjadi jembatan antara requirement, integration test, contract test, dan checklist review PR, sehingga skenario regresi lebih mudah diverifikasi secara konsisten.

Mengapa test narrative membantu skenario regresi

Unit test sangat efektif untuk memverifikasi logika lokal: fungsi murni, transformasi data, validasi, atau branch tertentu. Namun regresi nyata sering muncul di titik pertemuan antar-komponen:

  • frontend mengirim payload yang sedikit berbeda dari asumsi backend,
  • service A mengubah urutan event,
  • timezone atau waktu eksekusi mengubah hasil,
  • state lama di database memengaruhi perilaku baru,
  • proses async membuat hasil akhir sulit direproduksi.

Dalam kasus seperti ini, test yang terlalu rendah levelnya membuat niat bisnis sulit terlihat. Sebaliknya, narrative test memaksa tim menjawab tiga hal penting:

  1. Siapa aktornya dan dari state awal apa skenario dimulai?
  2. Aksi apa yang dilakukan dan observasi apa yang harus tetap benar?
  3. Boundary mana yang perlu diuji sebagai integration test, contract test, atau cukup checklist review?

Pendekatan ini mirip gaya man page atau referensi interaktif satir seperti Hacker Fables: ringkas, sistematis, dan fokus pada perilaku sistem, bukan pada cerita pemasaran. Format tersebut efektif karena pembaca bisa memindai precondition, input, output, failure mode, dan invariants dengan cepat.

Karakteristik test narrative yang baik

1. Ditulis dari sudut pandang perilaku, bukan implementasi

Hindari kalimat seperti "memanggil helper X lalu reducer Y menghasilkan field Z" jika yang sebenarnya penting adalah "pengguna tidak boleh melihat invoice sebagai paid sebelum payment provider mengirim konfirmasi final". Narrative harus tetap valid meski refactor internal berubah.

2. Punya state awal yang eksplisit

Banyak flaky test muncul karena state awal diasumsikan, bukan dinyatakan. Narrative yang baik selalu menyebut:

  • entitas yang sudah ada,
  • status awal masing-masing entitas,
  • dependensi eksternal yang disimulasikan,
  • waktu atau zona waktu yang relevan.

3. Membedakan langkah, observasi, dan invariant

Langkah adalah aksi. Observasi adalah hasil yang terlihat setelah aksi. Invariant adalah hal yang harus tetap benar sepanjang alur. Tiga hal ini tidak boleh dicampur.

4. Cukup ringkas untuk dibaca saat review PR

Jika satu narrative terlalu panjang, biasanya itu tanda bahwa skenario mencampur beberapa perilaku berbeda. Pisahkan menjadi beberapa narrative kecil yang masing-masing punya tujuan jelas.

Struktur test narrative yang praktis

Format berikut cukup sederhana untuk dipakai di repository biasa, wiki internal, atau dekat file test otomatis.

NRT-INV-001 — Invoice tidak menjadi paid sebelum settlement final

Tujuan:
Memastikan status invoice tetap pending saat callback payment masih bersifat provisional.

Aktor:
- Customer
- Payment Gateway
- Billing Service

State awal:
- Invoice INV-001 status=pending, amount=150000
- Order ORD-001 terkait ke INV-001
- Tidak ada payment final yang tersimpan
- Waktu sistem dibekukan pada 2025-01-10T09:00:00Z

Pemicu:
- Gateway mengirim callback dengan status=authorized

Ekspektasi terlihat:
- API GET /invoices/INV-001 tetap mengembalikan status=pending
- Audit log menyimpan event payment_authorized
- Tidak ada event invoice_paid yang dipublikasikan

Invariant:
- Invoice hanya boleh menjadi paid setelah callback final settlement/captured
- Callback ganda dengan payload sama tidak boleh menggandakan event bisnis

Boundary pengujian:
- Integration test: callback gateway → billing service → database → event bus
- Contract test: bentuk payload callback dari gateway
- PR checklist: perubahan mapping status tidak boleh mengubah invariant ini

Struktur di atas bekerja karena setiap bagian punya fungsi jelas:

  • Tujuan mencegah test dibuat tanpa alasan.
  • State awal mengurangi asumsi tersembunyi.
  • Pemicu memperjelas satu aksi utama.
  • Ekspektasi terlihat memfokuskan assertion pada hasil eksternal.
  • Invariant melindungi aturan bisnis dari perubahan implementasi.
  • Boundary pengujian membantu memetakan narasi ke jenis test yang tepat.

Menerjemahkan user flow kompleks menjadi test narrative

Masalah umum dalam regresi adalah satu flow terlihat sederhana bagi pengguna, tetapi sebenarnya melintasi banyak lapisan. Contoh: pengguna mengubah alamat pengiriman setelah order dibuat.

Langkah menerjemahkan flow

  1. Identifikasi momen yang bernilai bisnis.
    Jangan tulis semua klik atau semua method call. Ambil titik penting seperti alamat diganti sebelum label pengiriman dibuat atau alamat tidak boleh diubah setelah fulfillment terkunci.
  2. Tentukan state awal minimum.
    Cukup entitas yang relevan. Jangan isi fixture dengan data berlebih yang tidak berhubungan.
  3. Pilih observasi yang benar-benar terlihat dari luar.
    Respons API, event yang dipublikasikan, record database yang relevan, atau tampilan UI final. Hindari assertion ke detail internal yang mudah berubah.
  4. Nyatakan aturan yang tidak boleh rusak.
    Misalnya idempotensi, urutan status, atau larangan transisi tertentu.
  5. Petakan boundary teknis.
    Bagian mana cukup unit test, mana perlu integration test, mana perlu contract test terhadap dependency eksternal.

Contoh narrative untuk flow kompleks

NRT-ORD-004 — Alamat pengiriman boleh diubah sebelum fulfillment lock

Tujuan:
Mencegah regresi ketika perubahan alamat terjadi di sela sinkronisasi warehouse.

State awal:
- Order ORD-900 status=confirmed
- Shipment belum dibuat
- Fulfillment lock=false
- Customer memiliki alamat baru yang valid

Pemicu:
- Customer mengirim PATCH /orders/ORD-900/shipping-address

Ekspektasi terlihat:
- Response API sukses
- Order menyimpan alamat baru
- Event order_shipping_address_updated dipublikasikan sekali
- UI detail order menampilkan alamat baru setelah refresh

Invariant:
- Jika fulfillment lock=true, perubahan harus ditolak
- Perubahan alamat tidak boleh mengubah item, harga, atau metode pembayaran

Boundary pengujian:
- Integration test: API → service → database → event publish
- Contract test: payload event order_shipping_address_updated
- PR checklist: perubahan serializer/order mapper tidak menghapus field alamat penting

Narrative seperti ini lebih berguna daripada judul test yang terlalu umum seperti should update order successfully, karena pembaca langsung tahu regresi apa yang sedang dijaga.

Memetakan narrative ke integration test, contract test, dan checklist PR

Integration test: verifikasi perilaku lintas komponen

Gunakan integration test untuk memverifikasi boundary yang masih berada dalam kendali sistem Anda: API, service, database, queue, event bus internal, atau adapter ke dependency yang dimock secara terkontrol.

Contoh pseudocode:

describe('NRT-INV-001 invoice tetap pending pada callback provisional', () => {
  beforeEach(() => {
    freezeTime('2025-01-10T09:00:00Z')
    seedInvoice({ id: 'INV-001', status: 'pending', amount: 150000 })
    seedOrder({ id: 'ORD-001', invoiceId: 'INV-001' })
  })

  it('tidak menandai invoice sebagai paid saat status authorized', async () => {
    await post('/payment/callback', {
      invoice_id: 'INV-001',
      status: 'authorized',
      transaction_id: 'TX-123'
    })

    const invoice = await getInvoice('INV-001')
    expect(invoice.status).toBe('pending')

    expect(await hasAuditLog('payment_authorized', 'INV-001')).toBe(true)
    expect(await published('invoice_paid', { invoiceId: 'INV-001' })).toBe(false)
  })
})

Perhatikan bahwa assertion menargetkan perilaku eksternal yang stabil, bukan method internal yang dipanggil.

Contract test: verifikasi bentuk dan makna integrasi antar-service

Jika narrative menyebut payload callback atau event tertentu, contract test memverifikasi bahwa format data yang dipertukarkan tetap kompatibel. Ini penting ketika regresi terjadi bukan karena logika bisnis utama salah, tetapi karena field diganti nama, tipe berubah, atau nilai status dipetakan berbeda.

contract: payment callback provisional

expected fields:
- invoice_id: string
- status: one of [authorized, captured, settled, failed]
- transaction_id: string
- signature: string

business expectation:
- status=authorized diperlakukan sebagai non-final
- status=captured/settled diperlakukan sebagai final

Contract test tidak harus selalu memakai tool tertentu. Yang penting, ada spesifikasi yang dieksekusi otomatis dan dibagikan antar-pihak yang berintegrasi.

Checklist review PR: pengaman untuk area yang sulit diautomasi penuh

Tidak semua hal layak atau murah diuji end-to-end. Narrative bisa diturunkan menjadi checklist review PR agar reviewer tahu titik rawan regresi.

  • Apakah mapping status payment masih membedakan provisional vs final?
  • Apakah perubahan serializer tetap mempertahankan field invoice_id dan transaction_id?
  • Apakah idempotency key atau deduplikasi event tetap berjalan?
  • Apakah perubahan waktu, timezone, atau scheduler memengaruhi urutan update status?

Checklist bukan pengganti test otomatis, tetapi pelengkap yang berguna untuk area yang melibatkan migration data, perubahan observability, atau proses async yang tidak seluruhnya praktis diuji di level yang sama.

Penamaan kasus uji yang mudah dicari dan dipahami

Nama test harus menjelaskan perilaku, kondisi, dan hasil yang dijaga. Pola yang sering efektif:

  • [kode narrative] perilaku utama pada kondisi tertentu
  • given/when/then dalam bahasa natural
  • invariant-first naming untuk aturan bisnis kritis

Contoh nama yang baik:

  • NRT-INV-001 invoice tetap pending saat callback payment masih provisional
  • given fulfillment belum lock when shipping address diubah then event dipublikasikan sekali
  • invoice hanya menjadi paid setelah settlement final

Hindari nama seperti:

  • test payment callback
  • should work properly
  • update order 2

Nama yang spesifik mempermudah pencarian ketika bug lama muncul lagi, sekaligus memudahkan reviewer menghubungkan test dengan narrative yang melandasinya.

Fixture minimal: cukup untuk menjelaskan skenario

Fixture yang terlalu besar membuat test sulit dipahami dan rapuh saat model berubah. Prinsip dasarnya: hanya masukkan data yang memengaruhi outcome narasi.

Praktik yang disarankan

  • Buat factory atau builder dengan default aman, lalu override field yang relevan.
  • Nyatakan hanya status, relasi, dan identifier yang penting bagi skenario.
  • Hindari fixture global bersama yang diubah-ubah oleh banyak test.
  • Jika perlu data referensi, simpan sebagai seed kecil yang deterministik.
// contoh builder sederhana
const invoice = buildInvoice({
  id: 'INV-001',
  status: 'pending',
  amount: 150000
})

const order = buildOrder({
  id: 'ORD-001',
  invoiceId: invoice.id
})

Fixture minimal membantu karena ketika test gagal, Anda bisa lebih cepat mengerti apakah masalahnya ada di logika bisnis atau di setup yang terlalu rumit.

Membuat oracle dan assertion yang stabil

Oracle adalah sumber kebenaran yang Anda pakai untuk menilai hasil test. Pada regresi, oracle yang buruk membuat test sering gagal padahal perilaku bisnis masih benar, atau sebaliknya lolos padahal bug nyata ada.

Pilih assertion yang berorientasi hasil eksternal

Lebih stabil memeriksa:

  • status resource via API,
  • record database yang benar-benar bagian kontrak internal,
  • event yang dipublikasikan,
  • pesan error yang bersifat semantik.

Kurang stabil jika memeriksa:

  • urutan persis log internal yang tidak menjadi kontrak,
  • isi timestamp aktual sampai milidetik tanpa alasan kuat,
  • jumlah call ke helper internal hasil refactor,
  • snapshot besar yang berubah karena noise.

Gunakan invariant sebagai oracle utama

Misalnya pada payment flow, assertion terpenting bukan apakah fungsi parser dipanggil, melainkan:

  • invoice belum paid sebelum settlement final,
  • callback ganda tidak menggandakan efek samping,
  • status gagal tidak menerbitkan event sukses.

Invariant seperti ini cenderung bertahan lama meski implementasi berubah total.

Mengurangi flaky test akibat state tersembunyi dan dependensi waktu

Flaky test sering disalahartikan sebagai masalah framework, padahal akar utamanya biasanya state yang tidak terkendali. Pada test narrative, stabilitas sangat penting karena skenarionya cenderung melibatkan lebih banyak komponen.

Sumber flaky test yang umum

  • Waktu nyata: test bergantung pada jam sistem, timezone, atau race condition berbasis timeout.
  • State bersama: database, cache, file, queue, atau singleton tidak dibersihkan antar-test.
  • Asynchrony tak terkendali: event belum diproses ketika assertion dijalankan.
  • Urutan data tak deterministik: query tanpa pengurutan eksplisit.
  • Dependency eksternal: test memanggil layanan sungguhan yang lambat atau tidak stabil.

Cara menstabilkan narrative test

  1. Bekukan waktu.
    Gunakan clock yang bisa dikontrol dalam test. Jika sistem Anda memakai fungsi waktu langsung, buat lapisan kecil agar dapat di-inject.
  2. Reset state per test.
    Transaksi rollback, database ephemeral, namespace unik untuk cache, dan queue in-memory dapat membantu.
  3. Sinkronkan pekerjaan async.
    Alih-alih sleep, tunggu kondisi terukur: event selesai diproses, record muncul, atau job queue kosong.
  4. Tentukan ordering eksplisit.
    Jangan mengandalkan urutan default dari database atau koleksi.
  5. Stub dependency di boundary yang tepat.
    Untuk integration test, stub service eksternal tetapi tetap uji adapter dan mapping datanya.

Catatan: jika sebuah test narrative hanya lolos ketika dijalankan sendiri, hampir pasti ada state tersembunyi yang bocor dari atau ke test lain. Periksa singleton, cache process-level, environment variable, dan data yang tidak diisolasi per skenario.

Template test narrative yang bisa langsung dipakai

ID: NRT-<domain>-<nomor>
Judul: <perilaku yang ingin dijaga>

Tujuan:
<regresi apa yang dicegah dan mengapa itu penting>

Aktor:
- <user/service/sistem eksternal>

State awal:
- <entitas dan status awal>
- <kondisi waktu, data, flag, lock, atau relasi penting>

Pemicu:
- <request, event, job, atau aksi utama>

Ekspektasi terlihat:
- <respons API / perubahan state / output UI / event>
- <efek samping yang boleh atau tidak boleh terjadi>

Invariant:
- <aturan bisnis yang harus selalu benar>
- <syarat idempotensi, ordering, authorization, konsistensi>

Boundary pengujian:
- Unit test: <bagian logika lokal>
- Integration test: <alur antar-komponen>
- Contract test: <payload antar-service>
- PR checklist: <titik rawan yang harus dicek reviewer>

Catatan debugging:
- <sumber state tersembunyi atau dependency waktu yang perlu dikontrol>

Template ini sengaja tidak bergantung pada framework tertentu. Anda bisa menyimpannya sebagai file markdown di folder docs/testing/narratives, atau menaruhnya dekat suite test yang relevan.

Contoh workflow CI ringkas untuk test narrative

Workflow CI tidak perlu rumit. Yang penting, narrative bisa ditelusuri dari dokumen ke test otomatis dan hasil eksekusinya mudah dibaca.

steps:
  - checkout code
  - install dependencies
  - run lint dan static analysis
  - run unit tests
  - run contract tests
  - run integration tests bertanda narrative/regression
  - publish test report dengan ID narrative yang gagal

Contoh pembagian yang praktis:

  • Pull request cepat: unit test, contract test, dan subset integration test untuk narrative paling kritis.
  • Main branch / nightly: seluruh regression narrative suite.
  • Release candidate: narrative kritis lintas service ditambah smoke test environment staging.

Agar hasil CI berguna, sertakan ID narrative pada nama test atau laporan. Dengan begitu, ketika NRT-INV-001 gagal, tim bisa langsung membuka narasinya tanpa menebak konteks bisnis.

Kapan test narrative lebih membantu dibanding unit test biasa

Gunakan test narrative ketika salah satu kondisi berikut terjadi:

  • regresi melibatkan beberapa komponen atau boundary integrasi,
  • aturan bisnis lebih penting daripada detail implementasi,
  • flow sulit dipahami jika hanya melihat kumpulan unit test terpisah,
  • reviewer perlu cara cepat memverifikasi bahwa perubahan tidak merusak alur kritis,
  • bug yang sama berulang karena dokumentasi perilaku tidak eksplisit.

Namun bukan berarti semua hal harus dijadikan narrative test. Unit test tetap pilihan terbaik untuk:

  • logika murni yang kecil dan stabil,
  • branch condition yang banyak tetapi lokal,
  • validasi input sederhana,
  • fungsi utilitas atau transformasi data.

Trade-off-nya: narrative test lebih mahal dijalankan dan dirawat daripada unit test. Karena itu, simpan untuk alur yang benar-benar mewakili risiko regresi nyata. Jangan ubah setiap user story menjadi integration test besar jika satu invariant bisa diverifikasi lebih murah lewat unit test.

Kesalahan umum saat mulai menerapkan test narrative

  • Narrative terlalu abstrak.
    Akibatnya tidak bisa dipetakan ke test konkret.
  • Narrative terlalu teknis.
    Ia menjadi salinan implementasi, bukan dokumentasi perilaku.
  • Fixture terlalu besar.
    Test sulit dipahami dan rentan rusak saat model berubah.
  • Assertion memeriksa detail internal.
    Refactor kecil menyebabkan banyak false positive.
  • Tidak ada ID atau pengelompokan.
    Sulit melacak regresi mana yang dijaga oleh test mana.
  • Mencampur banyak tujuan dalam satu skenario.
    Saat gagal, diagnosis menjadi kabur.

Strategi adopsi bertahap di tim

Anda tidak perlu menulis ulang seluruh suite test. Mulailah dari 3-5 bug regresi yang paling sering muncul atau flow paling kritis secara bisnis. Untuk tiap kasus:

  1. tulis narrative singkat,
  2. identifikasi invariant,
  3. buat atau rapikan integration test yang memverifikasi invariant itu,
  4. tambahkan contract test jika ada boundary eksternal,
  5. tautkan ke checklist review PR.

Setelah beberapa iterasi, tim biasanya mulai punya bahasa bersama: "perubahan ini menyentuh NRT-ORD-004" lebih jelas daripada "tolong cek flow order ya". Itulah nilai utama pendekatan ini: bukan sekadar menambah test, tetapi membuat strategi verifikasi regresi lebih bisa dibaca, diaudit, dan dipertahankan.

Penutup

Test narrative efektif untuk menjelaskan dan menjaga skenario regresi yang terlalu kompleks jika dipecah hanya menjadi unit test biasa. Dengan struktur yang jelas, fixture minimal, assertion yang stabil, dan kontrol ketat terhadap state serta waktu, narrative dapat menjadi artefak yang dibaca manusia sekaligus sumber kebenaran bagi integration test, contract test, dan review PR.

Jika Anda ingin mulai praktis, ambil satu regresi nyata yang pernah menyakitkan tim, tulis narasinya dalam satu halaman, lalu pastikan ada pemetaan eksplisit ke test otomatis. Biasanya dari situ kualitas diskusi teknis meningkat jauh lebih cepat daripada sekadar menambah jumlah test tanpa konteks.