Mengurangi flaky test saat tim bekerja dalam ketidakpastian bukan sekadar masalah tooling. Dalam banyak tim, test mulai tidak stabil justru ketika struktur kerja berubah: reviewer berkurang, ownership modul tidak jelas, backlog bergeser, dan keputusan teknis tertunda. Hasilnya bukan hanya pipeline merah, tetapi juga turunnya kepercayaan pada suite test, meningkatnya rerun manual, dan release yang makin bergantung pada intuisi.

Masalah utamanya sederhana: ketika organisasi sedang tidak stabil, variasi kecil yang sebelumnya tertangani menjadi sumber kegagalan acak. Karena itu, fokusnya bukan hanya “memperbaiki test yang gagal”, melainkan membangun sistem verifikasi yang tetap dapat diandalkan meskipun kapasitas tim sedang turun. Artikel ini membahas sinyal awal flaky test, strategi test pyramid yang realistis, quarantine policy, retry yang terukur, stabilisasi environment CI, risk-based regression suite, serta workflow verifikasi sebelum merge dan release.

Mengapa ketidakpastian tim memperparah flaky test

Flaky test adalah test yang kadang lulus, kadang gagal, tanpa perubahan kode yang relevan. Dalam kondisi tim normal, flaky test sudah mahal. Dalam kondisi tidak pasti, dampaknya berlipat karena lebih sedikit orang yang punya waktu dan konteks untuk membedakan bug nyata dari noise.

Beberapa pola organisasi yang sering memicu masalah ini:

  • Ownership kabur: tidak jelas siapa yang bertanggung jawab atas test yang gagal, fixture yang rapuh, atau data setup yang bocor.
  • Perubahan prioritas mendadak: test maintenance dianggap pekerjaan sekunder sehingga akumulasi kerusakan kecil tidak tertangani.
  • Kapasitas review menurun: pull request lolos dengan test anti-pattern seperti sleep, dependency eksternal, atau assertion yang terlalu longgar.
  • Rotasi orang atau restrukturisasi: konteks environment CI, mock strategy, dan dependency antar-suite hilang bersama orang yang sebelumnya memegang area itu.

Efek praktisnya terlihat pada pipeline: rerun makin sering, durasi triage meningkat, merge tertunda, dan tim mulai menganggap kegagalan test sebagai hal biasa. Begitu kepercayaan pada sinyal test menurun, biaya debugging naik tajam.

Sinyal munculnya flaky test saat ownership mulai kabur

Flaky test jarang muncul sekaligus. Biasanya ada sinyal operasional yang dapat dipantau lebih awal.

1. Test gagal tetapi rerun langsung hijau

Ini sinyal paling jelas. Jika banyak job CI menjadi hijau hanya setelah dijalankan ulang tanpa perubahan apa pun, masalahnya bukan sekadar “server sedang sibuk”. Ada nondeterminism yang belum diisolasi.

2. Kegagalan terkonsentrasi pada area tanpa pemilik jelas

Misalnya integration test untuk autentikasi, pembayaran, atau background job sering gagal, tetapi tidak ada tim yang secara eksplisit memelihara fixture, fake service, atau data lifecycle di area itu.

3. Banyak pengecualian sementara di review

Contoh umum: “merge dulu, nanti test dibereskan”, “sementara tambahkan retry”, atau “skip test ini karena CI lambat”. Jika pengecualian seperti ini menjadi pola, suite sedang kehilangan disiplin.

4. Waktu diagnosis lebih lama daripada waktu perbaikan

Ketika engineer menghabiskan waktu 30-60 menit hanya untuk memastikan apakah kegagalan itu nyata atau flaky, berarti observability test dan struktur tanggung jawab belum sehat.

5. Test sensitif terhadap urutan eksekusi

Jika test lulus saat dijalankan sendiri tetapi gagal saat seluruh suite berjalan, biasanya ada state bersama yang bocor: database, cache, file system, clock, network stub, atau singleton di memory.

Penyebab teknis flaky test yang paling sering terabaikan

Ketidakpastian organisasi memperbesar masalah yang sebenarnya sudah ada di level teknis. Beberapa penyebab yang paling umum:

  • Ketergantungan waktu: penggunaan waktu aktual sistem, timeout terlalu ketat, scheduler, cron, atau timestamp yang diasumsikan unik.
  • Race condition: async process, event queue, background worker, WebSocket, atau polling yang tidak memiliki sinkronisasi jelas.
  • Shared state: data database tidak di-reset dengan benar, cache lintas test, port yang dipakai bersama, file temp yang tidak dibersihkan.
  • Dependency eksternal: memanggil service sungguhan, DNS, object storage, API pihak ketiga, atau jaringan internal yang tidak stabil.
  • Asersi terlalu lemah atau terlalu luas: memeriksa string lengkap yang tidak stabil, urutan data yang tidak dijamin, atau snapshot besar yang berubah karena field non-esensial.
  • Environment CI tidak konsisten: image runner berubah, resource terbatas, concurrency tinggi, seed acak tidak dicatat, atau konfigurasi locale/timezone berbeda.

Memahami akar masalah penting karena solusi seperti retry hanya menutupi gejala jika dipakai tanpa disiplin.

Test pyramid yang realistis dalam kondisi kapasitas tim menurun

Dalam masa tidak pasti, tim sering tergoda menaruh terlalu banyak verifikasi di end-to-end test karena terlihat paling dekat dengan perilaku pengguna. Ini biasanya justru memperburuk flaky test. Pendekatan yang lebih realistis adalah menjaga test pyramid tetap seimbang: lebih banyak test cepat dan deterministik di bawah, lebih sedikit test mahal di atas.

Lapisan yang disarankan

  • Unit test: mayoritas logika bisnis, murni, cepat, tanpa I/O. Cocok untuk menangkap regresi dengan biaya rendah.
  • Integration test: verifikasi kontrak antar-komponen penting seperti database, cache, queue, atau adapter API internal.
  • End-to-end test: hanya untuk alur bisnis kritikal yang benar-benar perlu dibuktikan lintas boundary.

Kapan harus mengurangi E2E

Jika tim sedang kekurangan reviewer atau maintainer, kurangi cakupan E2E yang redundan. Satu alur checkout utama lebih bernilai daripada sepuluh variasi UI yang menguji logika sama. Taruh variasi kondisi di unit atau integration test, dan sisakan E2E untuk smoke path yang paling berisiko.

Prinsip praktis

  • Setiap E2E harus punya alasan bisnis yang jelas.
  • Jika kegagalan bisa dideteksi lebih murah di integration test, turunkan levelnya.
  • Jangan menguji detail implementasi UI jika tujuan utamanya memvalidasi aturan bisnis.
  • Pisahkan suite cepat untuk merge dari suite lebih luas untuk jadwal berkala atau pra-release.

Test pyramid yang realistis bukan berarti menghapus E2E, tetapi menempatkan E2E sebagai verifikasi bernilai tinggi, bukan tempat menumpuk semua kekhawatiran.

Quarantine policy: bagaimana mengisolasi flaky test tanpa menyembunyikan masalah

Salah satu kesalahan umum adalah langsung men-disable test bermasalah tanpa jejak. Ini mengurangi kebisingan sesaat, tetapi menciptakan blind spot jangka panjang. Solusi yang lebih sehat adalah quarantine policy.

Tujuan quarantine

Quarantine berarti test yang teridentifikasi flaky dipisahkan dari jalur blokir utama, tetapi tetap dijalankan, dipantau, dan diberi owner serta tenggat perbaikan. Dengan begitu, sinyal merge tetap bersih tanpa menghilangkan visibilitas masalah.

Aturan minimum quarantine

  • Test hanya boleh masuk quarantine jika ada bukti flaky, misalnya gagal sporadis di beberapa run tanpa perubahan kode.
  • Setiap test di quarantine wajib punya issue, owner, dan tanggal evaluasi ulang.
  • Dashboard quarantine harus terlihat oleh seluruh tim.
  • Test yang terlalu lama di quarantine harus dievaluasi: perbaiki, turunkan levelnya, atau hapus jika memang tidak memberi nilai.

Contoh kebijakan tim

Policy: Flaky Test Quarantine

1. Test yang gagal sporadis minimal 2 kali dalam 10 run terakhir dapat ditandai flaky.
2. Maintainer wajib membuat issue berisi:
   - nama test / suite
   - gejala kegagalan
   - langkah reproduksi bila ada
   - dugaan akar masalah
   - owner perbaikan
3. Test di quarantine tidak memblokir merge utama,
   tetapi tetap dijalankan pada pipeline terpisah.
4. Maksimal masa quarantine: 14 hari kerja sebelum eskalasi.
5. Retry tidak boleh ditambahkan tanpa issue dan tanpa label flaky.
6. Release checklist harus menyebutkan flaky test aktif yang menyentuh area berisiko tinggi.

Kebijakan seperti ini bekerja karena menjaga dua hal sekaligus: kualitas sinyal CI dan akuntabilitas tim.

Retry yang terukur, bukan default untuk semua kegagalan

Retry sering dipakai sebagai solusi cepat. Dalam beberapa kasus, ini masuk akal, terutama untuk test yang terpengaruh latensi I/O atau startup environment. Namun retry yang diterapkan secara luas akan menutup akar masalah dan merusak metrik.

Kapan retry layak dipakai

  • Sebagai mitigasi sementara untuk test yang sudah ditandai flaky dan sedang diperbaiki.
  • Untuk operasi yang memang asynchronous, dengan syarat ada kondisi tunggu yang eksplisit dan terukur.
  • Pada level pipeline tertentu, bukan untuk semua suite dan semua test.

Kapan retry sebaiknya dihindari

  • Untuk unit test yang seharusnya deterministik.
  • Untuk menutupi race condition yang belum dipahami.
  • Jika retry membuat kegagalan nyata terlihat seperti noise.

Pola yang lebih baik daripada sleep tetap

Alih-alih menambahkan sleep 5 detik, gunakan polling dengan batas waktu dan kondisi yang jelas.

// contoh pseudocode polling yang lebih aman daripada sleep tetap
waitUntil(timeout = 10s, interval = 200ms) {
  order = repository.find(orderId)
  return order.status == "processed"
}
assert(order.status == "processed")

Pendekatan ini lebih baik karena menunggu kondisi bisnis yang diharapkan, bukan menebak durasi. Tetap ada trade-off: timeout yang terlalu longgar memperpanjang pipeline, terlalu ketat memicu gagal palsu.

Aturan operasional retry

  • Catat rerun rate per suite dan per test.
  • Pisahkan metrik “lulus setelah retry” dari “lulus pada percobaan pertama”.
  • Batasi jumlah retry agar biaya waktu tetap terkendali.
  • Review retry aktif secara berkala; jangan biarkan menjadi permanen.

Stabilisasi environment CI: kurangi variasi yang tidak relevan

Banyak flaky test bukan berasal dari logic test, melainkan dari environment CI yang berubah-ubah. Saat tim sedang sibuk, masalah ini sering luput karena dianggap infrastruktur semata.

Praktik yang membantu

  • Samakan timezone, locale, dan encoding antara lokal dan CI jika test sensitif terhadap format tanggal, sorting, atau string.
  • Jaga dependency tetap terkontrol dengan lockfile dan image yang konsisten.
  • Isolasi data per job agar parallel run tidak saling mengganggu.
  • Gunakan seed acak yang tercatat jika ada test berbasis data acak.
  • Kurangi dependency jaringan nyata dengan fake service, mock server, atau contract test.
  • Pastikan cleanup deterministik untuk database, cache, file, dan worker process.

Contoh langkah CI yang umum

# pseudocode pipeline generik
steps:
  - restore dependencies
  - start isolated test services
  - run database migrations for test database
  - seed deterministic fixtures
  - run unit tests
  - run integration tests
  - run smoke e2e tests
  - publish flaky-test report and rerun report
  - cleanup processes and temporary artifacts

Urutan seperti ini membantu karena setiap tahap memiliki boundary yang jelas. Jika integration test membutuhkan service tertentu, nyalakan secara eksplisit dan bersihkan setelah selesai. Hindari mengandalkan state runner yang tersisa dari job sebelumnya.

Kesalahan yang sering terjadi

  • Menggunakan environment bersama untuk beberapa pipeline.
  • Menjalankan test paralel tanpa partisi database atau resource.
  • Tidak mematikan background worker yang masih berjalan dari suite sebelumnya.
  • Snapshot atau fixture tidak diperbarui secara disiplin sehingga hasil test bergantung urutan eksekusi.

Risk-based regression suite untuk kondisi prioritas yang terus berubah

Saat prioritas bisnis bergerak cepat, tidak semua test layak dijalankan sebagai gerbang merge atau release. Tim perlu risk-based regression suite: subset test yang dipilih berdasarkan risiko, bukan jumlah sebanyak mungkin.

Cara menyusunnya

  1. Identifikasi area bisnis paling mahal jika gagal: autentikasi, pembayaran, otorisasi, checkout, sinkronisasi data, migrasi, dan notifikasi kritikal.
  2. Petakan dependency teknisnya: service, database table, queue, feature flag, dan integrasi eksternal.
  3. Pilih test yang benar-benar mendeteksi kegagalan bernilai tinggi dengan durasi serendah mungkin.
  4. Pisahkan menjadi beberapa level: pre-merge, nightly, dan pre-release.

Contoh struktur suite

  • Pre-merge: unit test utama, integration test inti, smoke E2E untuk alur kritikal.
  • Nightly: regression lebih luas, browser matrix, skenario data yang lebih kompleks.
  • Pre-release: verifikasi deployment path, migrasi, rollback readiness, dan kontrak integrasi penting.

Pendekatan ini efektif ketika kapasitas review menurun karena tim tetap menjaga proteksi pada area paling mahal, tanpa memaksa semua suite berjalan di setiap perubahan kecil.

Workflow verifikasi sebelum merge dan release

Ketika tim sedang dalam ketidakpastian, workflow yang eksplisit lebih penting daripada asumsi. Jangan mengandalkan “semua orang sudah tahu prosesnya”.

Sebelum merge

  1. Pastikan perubahan memiliki kategori risiko: rendah, sedang, atau tinggi.
  2. Jalankan suite minimum sesuai kategori risiko.
  3. Jika ada test gagal lalu hijau setelah rerun, tandai dalam laporan, jangan abaikan begitu saja.
  4. Reviewer memeriksa perubahan test, fixture, dan setup CI, bukan hanya kode aplikasi.
  5. Perubahan yang menyentuh async flow, concurrency, atau integrasi harus menyertakan strategi verifikasi tambahan.

Sebelum release

  1. Tinjau daftar test di quarantine yang berkaitan dengan area release.
  2. Pastikan tidak ada kegagalan berulang yang disamarkan retry.
  3. Verifikasi smoke path pada environment yang mendekati produksi.
  4. Tinjau perubahan pada feature flag, migrasi data, job background, dan dependency eksternal.
  5. Pastikan rollback atau mitigasi operasional tersedia jika ada risiko yang diterima.

Contoh policy singkat sebelum merge

Merge Verification Policy

- Low risk change:
  unit + selected integration tests
- Medium risk change:
  unit + integration + smoke e2e
- High risk change:
  all above + manual verification note or pair review

Rules:
- No silent rerun. Any rerun must be recorded.
- Any quarantined test touching the changed area must be acknowledged.
- Changes affecting async processing require deterministic wait/assert strategy.

Kebijakan seperti ini mengurangi keputusan ad hoc ketika kapasitas mental tim sedang terbagi.

Metrik yang perlu dipantau

Jika ingin mengurangi flaky test secara sistematis, tim perlu metrik operasional yang sederhana tetapi konsisten.

1. Failure rate

Persentase run yang gagal pada suite tertentu. Gunakan untuk melihat area yang paling berisik. Pisahkan kegagalan produk nyata dari flaky yang sudah diketahui bila memungkinkan.

2. Rerun rate

Seberapa sering job atau test perlu dijalankan ulang. Ini metrik penting karena rerun sering menjadi “biaya tersembunyi” yang tidak terlihat di dashboard kualitas biasa.

3. Mean time to diagnose

Waktu rata-rata sejak kegagalan muncul sampai engineer memahami apakah itu bug nyata, issue environment, atau flaky test. Jika metrik ini tinggi, masalahnya biasanya ada pada observability, ownership, atau dokumentasi workflow.

4. Quarantine count dan quarantine age

Berapa banyak test yang sedang diisolasi dan berapa lama mereka tinggal di sana. Jumlah yang terus naik menandakan tim sedang menunda utang kualitas.

5. First-pass pass rate

Persentase pipeline yang langsung hijau pada percobaan pertama. Ini lebih jujur daripada sekadar status akhir hijau setelah retry.

6. Suite duration by layer

Durasi unit, integration, dan E2E secara terpisah. Berguna untuk melihat apakah tim terlalu banyak meletakkan verifikasi di lapisan paling mahal.

Checklist implementasi yang bisa langsung dipakai

Checklist 30 hari

  • Petakan 10 test paling sering gagal dalam 2-4 minggu terakhir.
  • Tandai mana yang benar-benar flaky dan beri owner.
  • Buat quarantine policy tertulis dan aktifkan pipeline terpisah untuk test terisolasi.
  • Audit penggunaan sleep, timeout, dependency eksternal, dan shared state.
  • Pisahkan suite pre-merge dari nightly dan pre-release.
  • Definisikan smoke path kritikal untuk risk-based regression suite.
  • Mulai catat failure rate, rerun rate, dan mean time to diagnose.
  • Tambahkan aturan review untuk perubahan pada fixture, test helper, dan async flow.

Checklist teknis per test yang dicurigai flaky

  • Apakah test bergantung pada waktu sistem?
  • Apakah test memakai data acak tanpa seed yang tercatat?
  • Apakah ada network call sungguhan?
  • Apakah state dibersihkan setelah test?
  • Apakah test sensitif terhadap urutan eksekusi?
  • Apakah assertion terlalu luas atau tidak deterministik?
  • Apakah ada race condition karena worker, event, atau UI async?
  • Apakah retry dipakai tanpa issue dan owner?

Penutup

Mengurangi flaky test saat tim bekerja dalam ketidakpastian membutuhkan dua hal sekaligus: disiplin teknis dan kejelasan operasional. Test yang reliabel tidak muncul hanya dari framework yang tepat, tetapi dari boundary yang jelas, environment yang stabil, workflow verifikasi yang sederhana, dan akuntabilitas ketika sinyal mulai memburuk.

Jika kapasitas tim sedang turun, jangan mencoba menyelamatkan semuanya sekaligus. Mulailah dari area berisiko tinggi, bersihkan sinyal CI, batasi retry, karantina test bermasalah dengan aturan yang tegas, dan ukur failure rate, rerun rate, serta mean time to diagnose. Tujuannya bukan membuat suite sempurna, melainkan memastikan bahwa ketika test gagal, tim dapat mempercayai sinyalnya dan bertindak cepat.