Flaky test di CI membuat tim merasa selalu tertinggal: pull request tertahan, deploy tertunda, dan fokus engineering habis untuk menebak apakah kegagalan test itu bug sungguhan atau hanya noise. Solusi yang manusiawi bukan menambah lebih banyak aturan secara membabi buta, melainkan membangun sistem test yang bisa dipercaya, mudah ditriase, dan tidak mengandalkan heroics dari satu dua orang.

Jika CI sering merah karena test yang kadang gagal kadang lolos, akar masalahnya biasanya gabungan dari tiga hal: desain test yang rapuh, lingkungan eksekusi yang tidak terkendali, dan kebiasaan tim yang membiarkan noise menumpuk. Artikel ini fokus pada strategi praktis untuk tim kecil yang ingin memulihkan reliabilitas test suite tanpa menghentikan pengembangan fitur.

Konteks penting: tekanan kerja sering membuat tim memilih jalan pintas seperti menekan tombol rerun sampai hijau, mematikan assertion yang sensitif, atau membiarkan test bermasalah terlalu lama. Dalam jangka pendek itu terasa membantu, tetapi dalam jangka menengah CI kehilangan kredibilitas. Masalahnya bukan hanya pada tool, tetapi pada ownership dan kebiasaan tim.

Apa itu flaky test, dan mengapa dampaknya lebih besar dari sekadar gagal test

Flaky test adalah test yang hasilnya tidak konsisten untuk kode yang sama: kadang lulus, kadang gagal, tanpa perubahan bisnis yang relevan. Test seperti ini berbahaya karena merusak sinyal dari CI.

  • Bug nyata jadi tertutup noise: tim tidak cepat tahu mana kegagalan yang harus diperbaiki sekarang.
  • Waktu review dan merge membengkak: engineer harus rerun pipeline berulang kali.
  • Perilaku tim memburuk: orang mulai menganggap merah di CI itu normal.
  • Kepercayaan pada automation turun: keputusan release bergeser dari evidence ke feeling.

Tanda bahwa masalahnya sudah sistemik:

  • CI sering hijau setelah rerun tanpa perubahan kode.
  • Tim punya daftar test “memang suka begitu”.
  • Kegagalan paling sering muncul di integration atau end-to-end test yang berbagi state.
  • Engineer sulit menjelaskan apakah test memverifikasi bug atau hanya kondisi lingkungan.

Membedakan test gagal karena bug vs karena lingkungan

Langkah pertama yang paling penting adalah memperjelas klasifikasi kegagalan. Tanpa ini, semua masalah terlihat sama dan triase menjadi lambat.

Indikasi kegagalan karena bug aplikasi

  • Gagal secara deterministik di lokal dan CI.
  • Assertion menunjukkan output bisnis yang salah.
  • Reproduksi stabil dengan input yang sama.
  • Test lain yang memverifikasi alur serupa ikut gagal.

Indikasi kegagalan karena lingkungan atau desain test

  • Lulus saat dijalankan sendiri, gagal saat dijalankan paralel.
  • Gagal hanya di CI, terutama pada jam sibuk atau worker tertentu.
  • Berkaitan dengan timeout, urutan eksekusi, port, clock, race condition, atau data sisa.
  • Lulus setelah rerun tanpa perubahan kode.

Pertanyaan triase yang sebaiknya selalu ditanyakan

  1. Apakah test ini gagal konsisten jika dijalankan 10 kali?
  2. Apakah gagal hanya saat paralel?
  3. Apakah ada ketergantungan ke jam sistem, timezone, jaringan, atau service eksternal?
  4. Apakah test ini menulis atau membaca data yang bisa dipakai test lain?
  5. Apakah assertion terlalu dekat ke detail implementasi yang tidak stabil?

Klasifikasi sederhana yang berguna dalam tiket atau incident note:

  • Product defect: bug aplikasi.
  • Test defect: assertion salah atau setup test rapuh.
  • Environment defect: resource CI, network, clock, dependency eksternal.
  • Unknown: butuh data tambahan, jangan langsung rerun tanpa catatan.

Fondasi strategi: kurangi ketergantungan pada test mahal dengan test pyramid

Banyak flaky test muncul karena tim menaruh terlalu banyak beban pada integration atau end-to-end test. Test pyramid membantu memindahkan verifikasi ke level yang lebih cepat dan stabil.

Lapisan yang sehat

  • Unit test: paling banyak, cepat, deterministik, minim I/O.
  • Integration test: memverifikasi interaksi dengan database, message broker, file system, atau framework boundary.
  • End-to-end test: sedikit, fokus pada alur bisnis kritis.

Mengapa ini mengurangi flaky test?

  • Semakin tinggi lapisan test, semakin banyak sumber nondeterminisme.
  • Jika logika bisnis bisa divalidasi di unit test, Anda tidak perlu menunggu browser, jaringan, queue, dan service lain hanya untuk memeriksa aturan sederhana.
  • End-to-end test sebaiknya menguji integrasi nyata antar komponen, bukan seluruh detail validasi yang sudah tercover di bawahnya.

Contoh pembagian yang lebih stabil

Misalnya alur checkout gagal karena diskon tidak diterapkan. Jangan hanya mengandalkan satu test browser besar. Pecah verifikasinya:

  • Unit test untuk aturan diskon.
  • Integration test untuk penyimpanan order dan publish event.
  • Satu end-to-end test untuk memastikan alur checkout utama tetap tersambung.

Trade-off-nya: Anda harus disiplin mendesain boundary aplikasi. Namun biaya itu sebanding dengan meningkatnya kepercayaan pada CI.

Pola teknis utama untuk mengurangi flaky test

1. Isolasi data: setiap test punya dunia sendiri

State bersama adalah penyebab klasik flaky test. Test yang bergantung pada data sisa dari test lain akan sulit diprediksi, terutama saat eksekusi paralel.

Praktik yang disarankan:

  • Gunakan database atau schema terisolasi per job, atau transaksi yang di-rollback per test bila memungkinkan.
  • Buat data uji secara eksplisit di dalam setup test, jangan bergantung pada seed global yang berubah-ubah.
  • Gunakan identifier unik untuk resource yang dibuat saat test.
  • Bersihkan queue, cache, dan storage sementara sebelum atau sesudah suite yang relevan.
# contoh alur setup CI yang lebih aman secara konsep
# 1) siapkan database terisolasi
create_test_database app_test_${CI_JOB_ID}

# 2) jalankan migrasi
run_migrations --database app_test_${CI_JOB_ID}

# 3) eksekusi test dengan environment terikat
APP_DB=app_test_${CI_JOB_ID} run_test_suite

Jika memakai shared environment untuk beberapa job, kemungkinan race condition akan jauh lebih tinggi.

2. Isolasi clock dan waktu

Test yang memakai waktu nyata sering gagal karena perbedaan milidetik, timezone, daylight saving, atau batas waktu pemrosesan.

Praktik yang disarankan:

  • Bekukan waktu di test jika framework mendukung.
  • Hindari assertion yang terlalu presisi untuk timestamp.
  • Bandingkan rentang waktu atau waktu yang diinjeksi, bukan memanggil clock global langsung di logika bisnis.
// pseudo-code
clock = FixedClock("2026-01-15T10:00:00Z")
service = new BillingService(clock)
result = service.generateInvoice(user)
assert(result.dueDate == "2026-01-22T10:00:00Z")

Pendekatan ini bekerja karena test tidak lagi bergantung pada kondisi waktu saat runner CI mengeksekusinya.

3. Hindari ketergantungan langsung ke layanan eksternal

Service pihak ketiga, jaringan, dan API yang tidak Anda kontrol adalah sumber flaky test yang mahal. Untuk sebagian besar kasus, lebih baik memisahkan dua jenis verifikasi:

  • Contract test untuk memastikan format request-response dan ekspektasi antarlayanan tetap cocok.
  • Sedikit integration test nyata pada jadwal terpisah atau environment yang memang disiapkan untuk itu.

4. Gunakan contract test untuk integrasi yang sering berubah

Jika aplikasi Anda bergantung pada API internal atau microservice lain, contract test lebih stabil daripada end-to-end besar yang menyentuh terlalu banyak komponen sekaligus.

Yang diverifikasi dalam contract test:

  • struktur payload, field wajib, tipe data, dan skenario error penting;
  • ekspektasi yang dibutuhkan consumer, bukan seluruh perilaku provider;
  • kompatibilitas saat ada perubahan endpoint atau event schema.

Trade-off-nya: contract test tidak menggantikan verifikasi operasional penuh. Namun ia sangat efektif mengurangi kegagalan acak akibat dependency yang bergerak lebih cepat daripada aplikasi Anda.

5. Retry boleh, tetapi harus terukur

Retry bukan solusi utama. Retry hanya alat untuk meredam noise sementara sambil mengumpulkan bukti bahwa sebuah test memang flaky. Jika dipakai tanpa batas, retry menyembunyikan masalah.

Aturan praktis:

  • Retry hanya untuk kategori test tertentu, biasanya integration atau end-to-end.
  • Batasi jumlah retry, misalnya satu kali, bukan tak terbatas.
  • Catat apakah test lulus setelah retry; ini harus muncul sebagai sinyal, bukan dianggap lulus bersih.
  • Jangan pakai retry pada unit test. Unit test harus deterministik.
# pseudo-config CI
steps:
  - run: unit-tests --no-retry
  - run: integration-tests --retry=1 --report-flaky
  - run: e2e-tests --retry=1 --quarantine-file=.ci/quarantined-tests.txt

Mengapa pendekatan ini lebih sehat? Karena Anda tetap menjaga pipeline bergerak, tetapi data flaky rate tetap terlihat dan menjadi pekerjaan perbaikan yang nyata.

6. Quarantine test: pisahkan, jangan kubur

Quarantine berarti memindahkan test yang terbukti flaky ke jalur terpisah agar tidak terus-menerus memblokir delivery, sambil tetap dipantau dan diberi tenggat perbaikan.

Prinsip quarantine yang benar:

  • Harus ada label, owner, alasan, dan tanggal masuk quarantine.
  • Test tetap dijalankan di job terpisah dan hasilnya tetap terlihat.
  • Ada SLA internal, misalnya wajib ditinjau dalam beberapa hari kerja.
  • Quarantine bukan tempat pembuangan permanen.

Anti-pattern yang sering terjadi:

  • Menandai test sebagai skip tanpa tiket dan tanpa owner.
  • Menganggap quarantine sebagai “selesai”.
  • Menggunakan quarantine untuk menyembunyikan regresi produk nyata.

Contoh alur CI yang lebih manusiawi

Tujuan alur ini adalah memaksimalkan sinyal cepat, memisahkan noise, dan membuat tindakan setelah kegagalan menjadi jelas.

Tahap yang direkomendasikan

  1. Fast checks: lint, type check, unit test. Harus cepat dan sangat reliabel.
  2. Integration checks: database, queue, cache, filesystem, contract test.
  3. Selective end-to-end: hanya alur bisnis kritis.
  4. Quarantine lane: test yang diketahui flaky, tidak memblokir merge tetapi dilaporkan.
  5. Nightly full suite: cakupan lebih lebar, termasuk skenario lambat atau bergantung environment tertentu.
# pseudo-pipeline
pipeline:
  fast_checks:
    - lint
    - typecheck
    - unit_tests

  integration:
    - db_tests
    - queue_tests
    - contract_tests

  critical_e2e:
    - checkout_flow
    - login_flow

  quarantine_non_blocking:
    - run_quarantined_tests

  nightly:
    - full_e2e
    - cross-service_smoke

Kelebihan pendekatan ini:

  • Engineer mendapat feedback cepat dari lapisan yang paling stabil.
  • Kegagalan mudah dipetakan ke area masalah.
  • Tim bisa menjaga throughput tanpa berpura-pura bahwa semua test sama kualitasnya.

Review checklist untuk mencegah flaky test sejak awal

Flaky test lebih murah dicegah daripada diperbaiki. Tambahkan checklist singkat ini saat code review:

  • Apakah test ini bergantung pada urutan eksekusi?
  • Apakah test memakai waktu nyata, sleep, atau timeout kasar?
  • Apakah test mengakses jaringan atau service eksternal yang bisa di-stub atau diganti contract test?
  • Apakah data uji dibuat eksplisit dan diisolasi?
  • Apakah assertion memverifikasi perilaku bisnis, bukan detail implementasi yang mudah berubah?
  • Apakah test baru ini berada di lapisan yang tepat, atau sebenarnya bisa diturunkan ke unit/integration?
  • Jika ada retry, apakah alasannya jelas dan metriknya dipantau?

Checklist ini terdengar sederhana, tetapi sangat efektif membentuk kebiasaan tim.

Workflow triase regresi yang jelas

Salah satu penyebab tim terasa selalu tertinggal adalah tidak adanya workflow triase yang tegas. Semua orang sibuk, tetapi tidak ada keputusan cepat tentang siapa melakukan apa ketika CI merah.

Proses triase yang bisa langsung dipakai

  1. Deteksi: CI menandai job gagal dan mengelompokkan test yang gagal.
  2. Klasifikasi awal: bug aplikasi, test defect, environment defect, atau unknown.
  3. Reproduksi minimal: jalankan test yang sama beberapa kali, sendiri dan paralel bila relevan.
  4. Keputusan:
    • Jika bug aplikasi: perbaiki atau rollback perubahan terkait.
    • Jika test defect: buat perbaikan segera, atau quarantine dengan owner.
    • Jika environment defect: buka incident kecil untuk runner, resource, network, atau dependency.
  5. Pencatatan: simpan akar masalah, bukan hanya status lulus/gagal.
  6. Pencegahan: tambahkan guardrail, checklist, atau refactor test layer.

Siapa yang memiliki masalahnya?

Ownership perlu jelas tetapi tidak menghukum. Model yang biasanya efektif untuk tim kecil:

  • Author perubahan bertanggung jawab melakukan klasifikasi awal.
  • Test owner atau module owner membantu jika kegagalan menyentuh area yang dikenal rapuh.
  • Satu rotasi mingguan bertanggung jawab pada kesehatan CI secara umum, bukan memperbaiki semua bug sendirian.

Pendekatan ini lebih manusiawi karena tidak mengandalkan satu orang “penjaga CI” sepanjang waktu, tetapi tetap menjaga akuntabilitas.

Metrik reliabilitas test suite yang benar-benar berguna

Jangan hanya mengukur berapa banyak test yang ada. Ukur apakah suite bisa dipercaya.

Metrik inti

  • Pass rate per suite: unit, integration, e2e.
  • Flaky rate per test: seberapa sering test gagal lalu lulus pada rerun.
  • Mean time to triage: waktu dari CI merah sampai klasifikasi awal.
  • Mean time to fix: waktu dari identifikasi flaky sampai diperbaiki atau dikeluarkan dari quarantine.
  • Quarantine count: jumlah test dalam quarantine dan usianya.
  • False positive failure rate: proporsi kegagalan yang bukan bug produk.

Cara memakai metrik tanpa membuat tim defensif

  • Gunakan metrik untuk perbaikan sistem, bukan menyalahkan individu.
  • Tampilkan tren mingguan, bukan snapshot harian yang mudah menyesatkan.
  • Bedakan metrik blocking lane dan non-blocking lane.

Jika Anda hanya melihat angka total kegagalan, Anda akan sulit tahu apakah masalah membaik atau sekadar berpindah tempat.

Anti-pattern yang perlu dihentikan

  • Rerun sampai hijau tanpa investigasi: ini menghapus sinyal akar masalah.
  • Sleep acak untuk “menstabilkan” test: biasanya hanya memperlambat suite tanpa menghilangkan race condition.
  • Shared mutable state: database, cache, file, atau queue dipakai banyak test tanpa isolasi.
  • Assertion terlalu luas atau terlalu rapuh: memeriksa seluruh payload besar padahal yang penting hanya beberapa field bisnis.
  • Terlalu banyak end-to-end test: semua validasi ditumpuk pada lapisan paling lambat dan paling rapuh.
  • Skip permanen: test dimatikan lalu dilupakan.
  • Tidak ada owner untuk test flaky: semua orang tahu ada masalah, tak seorang pun menindaklanjuti.

Langkah implementasi 30 hari untuk tim kecil

Berikut rencana realistis untuk tim yang belum punya bandwidth besar.

Minggu 1: buat masalahnya terlihat

  1. Inventarisasi 10-20 test paling sering gagal di CI.
  2. Kelompokkan per jenis: unit, integration, e2e.
  3. Tambahkan label sederhana: bug, test, environment, unknown.
  4. Sepakati definisi quarantine dan aturan retry.
  5. Tunjuk rotasi mingguan untuk triase CI.

Minggu 2: stabilkan jalur utama

  1. Pisahkan fast checks dari integration dan e2e.
  2. Nonaktifkan retry pada unit test.
  3. Karantina test paling noisy yang memblokir delivery, dengan owner dan tiket.
  4. Perbaiki 2-3 flaky test yang paling sering muncul untuk membangun momentum.

Minggu 3: perbaiki akar masalah teknis

  1. Refactor test yang berbagi data atau bergantung urutan eksekusi.
  2. Bekukan clock pada test yang sensitif waktu.
  3. Ganti sebagian integrasi eksternal dengan contract test atau stub yang tepat.
  4. Tinjau ulang apakah beberapa e2e bisa diturunkan ke integration atau unit test.

Minggu 4: institusikan kebiasaan yang sehat

  1. Tambahkan review checklist untuk test baru.
  2. Publikasikan metrik sederhana: flaky rate, quarantine count, mean time to triage.
  3. Bahas 30 menit tiap minggu khusus kesehatan CI.
  4. Keluarkan test dari quarantine hanya setelah ada bukti stabil.

Target 30 hari bukan membuat suite sempurna. Targetnya adalah mengembalikan kepercayaan bahwa merah di CI berarti sesuatu yang bisa ditindaklanjuti.

Penutup

Strategi test yang manusiawi untuk mengurangi flaky test di CI berarti menerima bahwa masalah ini bukan semata soal framework atau runner. Ia menyangkut desain test, batas sistem, alur kerja triase, dan budaya ownership. Tim yang merasa selalu tertinggal biasanya bukan kekurangan alat, tetapi kekurangan sinyal yang bisa dipercaya.

Mulailah dari hal yang paling berdampak: bedakan bug vs environment, rapikan test pyramid, batasi retry, gunakan quarantine dengan disiplin, isolasi data dan clock, lalu ukur reliabilitas suite secara eksplisit. Jika dilakukan konsisten, CI tidak lagi menjadi sumber kecemasan harian, melainkan alat bantu keputusan yang benar-benar berguna.