Flaky test di CI adalah test yang kadang lulus dan kadang gagal tanpa perubahan kode yang relevan. Untuk tim backend, masalah ini bukan sekadar gangguan kecil: flaky test mengurangi kepercayaan pada pipeline, mendorong kebiasaan rerun tanpa analisis, dan menambah lead time karena engineer harus membedakan kegagalan nyata dari noise.
Cara menguranginya bukan dengan menambah retry secara membabi buta. Pendekatan yang lebih efektif adalah mengidentifikasi pola flaky, mengukur dampaknya, mereproduksi penyebabnya secara terkontrol, lalu memperbaiki sumber nondeterminism pada test atau sistem di sekitarnya. Artikel ini fokus pada langkah praktis yang bisa diterapkan tim backend di pipeline CI umum, tanpa bergantung pada satu framework tertentu.
Apa Itu Flaky Test dan Mengapa Berbahaya
Flaky test adalah test yang hasilnya tidak stabil pada kondisi yang seharusnya sama. Satu commit yang sama bisa gagal di satu job CI, lalu lulus pada rerun berikutnya. Ini berbeda dari test yang memang rusak secara konsisten.
Dampaknya biasanya terasa di tiga area:
- Kepercayaan tim menurun: engineer mulai menganggap merah di CI sebagai hal biasa.
- Lead time membengkak: PR tertahan karena rerun, investigasi, atau menunggu job tambahan.
- Sinyal kualitas melemah: bug nyata bisa tertutup oleh kebiasaan mengabaikan kegagalan test.
Tanda umum bahwa suite test sudah terkena masalah flaky:
- Test yang gagal berpindah-pindah tanpa pola jelas.
- Kegagalan hanya muncul di CI, jarang atau tidak pernah muncul di lokal.
- Rerun job sering membuat pipeline hijau tanpa perubahan apa pun.
- Test makin sering gagal setelah parallel execution ditingkatkan.
Cara Mengidentifikasi dan Mengukur Flaky Test di CI
Langkah pertama bukan langsung memperbaiki semua test, tetapi membangun visibilitas. Tim perlu tahu test mana yang paling sering gagal secara tidak konsisten, pada kondisi apa, dan seberapa besar dampaknya pada pipeline.
Metrik yang Perlu Dicatat
Beberapa metrik praktis yang berguna:
- Failure rate per test: seberapa sering test gagal.
- Pass-after-rerun rate: test gagal di percobaan pertama lalu lulus saat diulang.
- Waktu kegagalan: apakah sering muncul pada jam tertentu, saat beban CI tinggi, atau pada branch tertentu.
- Node/worker affinity: apakah kegagalan lebih sering muncul di worker atau environment tertentu.
- Durasi test: test lambat sering lebih rentan timeout atau race condition.
Jika CI Anda menyimpan output JUnit, JSON report, atau log terstruktur lain, gunakan itu untuk menandai test yang gagal tidak konsisten selama beberapa hari atau minggu. Tujuannya bukan mencari presisi statistik tinggi, tetapi menemukan kandidat perbaikan dengan dampak terbesar.
Bedakan Flaky Test dari Bug Nyata
Jangan semua kegagalan yang sulit direproduksi langsung dilabeli flaky. Kemungkinan lain adalah bug concurrency yang memang ada di aplikasi, ketidakstabilan environment, atau dependency eksternal yang berubah perilakunya. Label flaky seharusnya berarti hasil test tidak deterministik, bukan sekadar penyebabnya belum diketahui.
Buat Daftar Prioritas
Prioritaskan test berdasarkan kombinasi berikut:
- Sering gagal di CI.
- Menghambat jalur kritis seperti merge ke branch utama.
- Menutup area penting seperti autentikasi, pembayaran, antrian, atau migrasi data.
- Sulit dibedakan dari kegagalan produk yang nyata.
Dengan cara ini, tim tidak tenggelam dalam pembersihan total suite, tetapi fokus pada sumber gangguan terbesar terlebih dahulu.
Sumber Umum Flaky Test pada Sistem Backend
1. Race Condition
Race condition muncul saat hasil test bergantung pada urutan eksekusi yang tidak dijamin. Ini sering terjadi pada job asynchronous, worker, event handler, cache invalidation, dan operasi database yang berjalan paralel.
Contoh gejala:
- Test kadang membaca data sebelum proses background selesai.
- Assert dilakukan terlalu cepat setelah publish event atau enqueue job.
- Urutan record berbeda saat query tidak memiliki ordering eksplisit.
Mengapa ini terjadi? Karena test mengasumsikan sinkronisasi yang sebenarnya tidak ada. Jika sistem butuh menunggu kondisi tertentu, test harus menunggu sinyal yang benar, bukan mengandalkan jeda waktu acak.
2. Ketergantungan Waktu
Test backend sering menyentuh timestamp, timezone, timeout, TTL cache, token expiry, scheduler, atau logic berbasis jam. Flaky muncul saat test bergantung pada waktu aktual mesin yang terus bergerak.
Contoh gejala:
- Test gagal di pergantian menit atau tanggal.
- Perbedaan timezone antara lokal dan CI.
- Assert timeout terlalu ketat untuk lingkungan CI yang lebih lambat.
Praktik yang lebih aman adalah mengontrol waktu pada level test, misalnya dengan menyediakan clock yang dapat diinjeksi ke kode aplikasi.
3. Network dan External API
Ketergantungan pada layanan eksternal adalah sumber flaky yang sangat umum. Latensi jaringan, rate limit, DNS, sertifikat, autentikasi, atau perubahan response pihak ketiga dapat membuat test tidak stabil.
Untuk sebagian besar test di CI, dependency eksternal sebaiknya diisolasi dengan stub, fake, atau mock di boundary yang jelas. Integration test ke layanan nyata tetap berguna, tetapi sebaiknya terbatas, diberi label khusus, dan tidak dijadikan sinyal utama untuk setiap perubahan kecil.
4. Shared State
Test yang berbagi state melalui database, filesystem, cache, message broker, environment variable, atau singleton proses sangat rentan flaky. Ketika satu test meninggalkan state sisa, test lain bisa gagal tergantung urutan eksekusi.
Gejala khasnya adalah test lulus saat dijalankan sendiri, tetapi gagal saat seluruh suite berjalan.
5. Data Test Tidak Terisolasi
Mengandalkan data global seperti user dengan email tetap, ID hardcoded, atau tabel yang tidak dibersihkan sering menimbulkan konflik. Ini makin parah saat CI menjalankan shard atau worker paralel.
Data test seharusnya unik per test atau per worker, dan pembersihan state harus konsisten.
6. Parallel Execution
Parallel execution mempercepat pipeline, tetapi juga mengekspos asumsi tersembunyi. Test yang diam-diam bergantung pada port tetap, nama file tetap, database bersama, atau urutan global akan mulai gagal ketika dijalankan bersamaan.
Parallelism bukan penyebab utama, tetapi alat yang memperlihatkan nondeterminism yang sebelumnya tersamar.
Workflow Verifikasi Flaky Test yang Praktis
Saat menemukan kandidat flaky, gunakan workflow yang konsisten. Tujuannya adalah mengubah dugaan menjadi bukti, lalu menentukan apakah masalah ada di test, environment, atau aplikasi.
1. Reproduksi Lokal
Ambil commit yang sama dengan yang gagal di CI, lalu jalankan test target berulang kali di lokal. Jika test hanya gagal saat suite penuh berjalan, jalankan bersama subset test yang relevan atau dengan parallelism yang mirip CI.
Hal yang perlu disamakan dengan CI:
- Environment variable penting.
- Timezone dan locale.
- Jumlah worker/paralelisme.
- Jenis database atau service pendukung.
- Batas resource seperti CPU dan memori jika memungkinkan.
2. Rerun Terkontrol
Rerun berguna sebagai alat diagnosis, bukan solusi akhir. Lakukan rerun dengan aturan yang jelas:
- Ulang test yang sama beberapa kali pada commit yang sama.
- Pisahkan rerun satu test dari rerun seluruh job.
- Catat apakah kegagalan muncul pada worker tertentu atau kondisi tertentu.
Jika test gagal 1 dari 20 kali pada kondisi yang relatif sama, itu sinyal kuat adanya nondeterminism.
3. Tambahkan Logging yang Relevan
Logging untuk investigasi flaky harus fokus pada state yang memengaruhi hasil. Hindari menambah log terlalu banyak tanpa struktur.
Contoh informasi yang sering membantu:
- ID test atau correlation ID.
- Timestamp penting dan zona waktu yang dipakai.
- Nama worker, shard, atau host CI.
- State database atau message queue sebelum dan sesudah langkah penting.
- Status retry pada operasi I/O.
Log yang baik membantu membedakan antara timeout, urutan event yang salah, dan kebocoran state.
4. Tagging dan Klasifikasi
Beri tag pada test berdasarkan karakteristiknya, misalnya unit, integration, db, external, slow, atau quarantine. Ini memudahkan analisis dan keputusan CI, misalnya test yang menyentuh layanan eksternal dijalankan terpisah dari suite utama.
5. Quarantine Test Secara Terbatas
Quarantine berarti memisahkan test yang diketahui flaky dari gate utama sambil tetap dipantau. Ini berguna agar pipeline utama tetap memberikan sinyal yang lebih bersih, tetapi ada trade-off: coverage praktis terhadap area tersebut menurun jika quarantine dibiarkan terlalu lama.
Gunakan quarantine sebagai langkah sementara, bukan tempat parkir permanen. Setiap test yang di-quarantine perlu owner, tiket perbaikan, dan batas waktu evaluasi.
6. Perbaikan Berdasarkan Prioritas
Setelah akar masalah ditemukan, perbaiki mulai dari test dengan dampak terbesar. Jangan mencoba merombak seluruh suite sekaligus. Dalam banyak tim, memperbaiki 10-20 test paling bermasalah sudah cukup untuk menaikkan kepercayaan terhadap CI secara signifikan.
7. Pencegahan Regresi
Setelah test diperbaiki, tambahkan mekanisme agar pola serupa tidak muncul lagi:
- Review checklist untuk test baru.
- Rule lint atau helper test standar.
- Pemisahan kategori test dan kebijakan CI yang konsisten.
- Pelacakan pass-after-rerun rate per minggu.
Strategi Perbaikan yang Efektif
Hilangkan Ketergantungan pada Sleep Tetap
Salah satu penyebab flaky paling umum adalah penggunaan sleep atau jeda tetap untuk menunggu kondisi. Ini rapuh: terlalu pendek menyebabkan gagal, terlalu panjang memperlambat suite.
Lebih baik gunakan polling dengan batas waktu yang jelas terhadap kondisi yang memang ingin diverifikasi.
# contoh pseudocode umum, tidak bergantung framework tertentu
max_wait = 5 seconds
interval = 100 milliseconds
start = now()
while now() - start < max_wait:
result = query_order_status(order_id)
if result == "processed":
break
wait(interval)
assert result == "processed"Mengapa ini lebih baik? Karena test menunggu kondisi bisnis yang benar-benar relevan, bukan menebak durasi yang mungkin cukup pada mesin tertentu.
Kontrol Waktu dengan Clock yang Dapat Diinjeksi
Jika logika aplikasi bergantung pada waktu, hindari memanggil waktu sistem langsung di banyak tempat. Bungkus akses waktu dalam abstraksi sederhana, lalu injeksikan implementasi tetap saat test.
// pseudocode
interface Clock {
now()
}
class SystemClock implements Clock {
now() { return current_time() }
}
class FixedClock implements Clock {
constructor(fixedTime) { this.fixedTime = fixedTime }
now() { return this.fixedTime }
}Dengan pendekatan ini, test untuk expiry token, scheduler, atau cache TTL menjadi deterministik dan tidak bergantung pada waktu aktual runner CI.
Isolasi Dependency Eksternal
Untuk test yang memverifikasi business logic internal, mock atau stub dependency eksternal pada boundary yang tepat. Misalnya, aplikasi Anda punya komponen payment gateway client; pada test, gantikan komponen itu dengan fake yang mengembalikan response terprediksi.
Pilih pendekatan sesuai tujuan:
- Unit test: gunakan fake atau stub agar cepat dan deterministik.
- Integration test internal: gunakan service nyata yang dikontrol sendiri, misalnya database atau broker lokal/ephemeral.
- End-to-end ke pihak ketiga: batasi, beri tag khusus, dan jangan jadikan gate utama untuk semua PR jika reliabilitasnya rendah.
Isolasi Data dan State per Test
Test yang baik tidak bergantung pada urutan. Praktik yang membantu:
- Buat data unik per test, misalnya email atau key dengan suffix acak.
- Reset database secara konsisten per test atau per kelas suite.
- Gunakan namespace terpisah untuk cache, topic, atau queue per worker.
- Hindari singleton mutable yang bertahan antar test dalam proses yang sama.
Jika suite besar, isolation penuh per test bisa lebih lambat. Trade-off ini bisa diatasi dengan isolation per worker atau per file suite, selama state antar unit eksekusi tetap tidak bocor.
Buat Query dan Assert Lebih Deterministik
Jangan mengasumsikan urutan hasil query tanpa ORDER BY eksplisit. Jangan mengassert seluruh payload besar jika yang penting hanya subset field tertentu. Jangan mengandalkan representasi string tanggal yang bisa berbeda timezone atau locale.
Perbaikan kecil seperti ini sering menghilangkan flaky yang tampak misterius.
Tangani Parallel Execution dengan Sadar
Jika CI menjalankan test paralel, pastikan resource berikut tidak berbenturan:
- Port jaringan.
- Nama file sementara.
- Database/schema.
- Queue/topic.
- Cache key prefix.
Strategi umum adalah memberi suffix berdasarkan nomor worker atau job ID CI.
# contoh pseudocode environment setup
TEST_RUN_ID = env("CI_JOB_ID", random_id())
WORKER_ID = env("CI_WORKER", "0")
DB_NAME = "app_test_" + TEST_RUN_ID + "_" + WORKER_ID
CACHE_PREFIX = "test:" + TEST_RUN_ID + ":" + WORKER_ID + ":"
TMP_DIR = "/tmp/app-test-" + TEST_RUN_ID + "-" + WORKER_IDDengan demikian, test paralel tidak saling menimpa state.
Contoh Implementasi di CI Secara Umum
Berikut contoh alur CI generik yang bisa diadaptasi ke banyak platform:
- Jalankan suite utama dan simpan hasil dalam format terstruktur.
- Jika ada kegagalan, identifikasi test yang gagal, lalu rerun hanya test tersebut sekali atau beberapa kali secara terkontrol.
- Tandai test sebagai kandidat flaky jika gagal lalu lulus pada rerun untuk commit yang sama.
- Simpan statistik harian untuk laporan mingguan.
- Jika test sudah masuk daftar quarantine, jalankan di job terpisah agar tetap terpantau.
# pseudocode pipeline umum
steps:
- run_tests --report test-report.json
- if failed:
extract_failed_tests test-report.json > failed-tests.txt
rerun_tests --from-file failed-tests.txt --report rerun-report.json
analyze_flakiness test-report.json rerun-report.json > flaky-summary.json
- publish_artifacts test-report.json rerun-report.json flaky-summary.json
- run_quarantined_tests --non-blockingHal penting di sini bukan nama command, tetapi pola operasionalnya:
- Rerun dibatasi agar tidak menyamarkan bug nyata.
- Artefak disimpan agar investigasi tidak bergantung pada log yang cepat hilang.
- Quarantine dipisah dari gate utama, tetapi tetap terlihat.
Kapan Retry Boleh Dipakai?
Retry di level CI atau test runner kadang berguna untuk menjaga produktivitas jangka pendek, terutama saat tim sedang membersihkan utang flaky. Namun retry harus dipandang sebagai kompensasi sementara, bukan perbaikan.
Gunakan retry dengan hati-hati jika:
- Ada bukti kegagalan berasal dari faktor nondeterministik yang sudah diketahui.
- Retry dilengkapi pelacakan metrik pass-after-rerun.
- Test yang terlalu sering mengandalkan retry otomatis masuk backlog perbaikan.
Jangan gunakan retry sebagai alasan untuk membiarkan test rapuh tetap ada berbulan-bulan.
Checklist Audit Suite Test
Gunakan checklist berikut untuk menilai apakah suite Anda rentan terhadap flaky test di CI:
- Apakah ada test yang memakai
sleeptetap untuk menunggu async process? - Apakah semua query yang mengandalkan urutan hasil sudah menggunakan ordering eksplisit?
- Apakah test yang menyentuh waktu memakai clock yang dapat dikontrol?
- Apakah ada dependency langsung ke external API dalam suite utama?
- Apakah database, cache, filesystem, dan queue dibersihkan atau diisolasi dengan benar?
- Apakah test lulus saat dijalankan sendiri tetapi gagal saat suite penuh atau paralel?
- Apakah resource seperti port, file path, dan key cache unik per worker?
- Apakah report CI menyimpan nama test, durasi, worker, dan artefak log?
- Apakah ada daftar test quarantine beserta owner dan target perbaikan?
- Apakah tim memantau test yang gagal lalu lulus pada rerun?
Praktik Desain Test yang Deterministik
Mengurangi flaky test di CI akan lebih mudah jika test sejak awal dirancang deterministik. Prinsip utamanya adalah hasil test harus ditentukan oleh input dan state yang jelas, bukan kondisi kebetulan di lingkungan eksekusi.
Prinsip yang Perlu Dijaga
- Satu test, satu alasan gagal: hindari test terlalu panjang dengan banyak assert tak terkait.
- Kontrol dependency nondeterministik: waktu, random, network, dan concurrency harus dapat diatur.
- State lokal: test membuat dan membersihkan state-nya sendiri.
- Assert pada perilaku penting: fokus pada kontrak bisnis, bukan detail incidental yang mudah berubah.
- Minimalkan shared fixture mutable: fixture global sering menjadi sumber kebocoran state.
Kesalahan Umum yang Sering Terjadi
- Menggunakan data test statis yang dipakai banyak test.
- Mengandalkan urutan eksekusi antar test.
- Menggabungkan verifikasi unit dan integration dalam satu test.
- Memanggil layanan eksternal langsung dari test yang seharusnya cepat dan stabil.
- Menulis timeout terlalu agresif berdasarkan performa laptop lokal.
Penutup
Strategi mengurangi flaky test di CI untuk tim backend dimulai dari observabilitas, bukan tebakan. Ukur test yang tidak stabil, reproduksi secara terkontrol, kelompokkan berdasarkan sumber masalah, lalu perbaiki akar nondeterminism seperti race condition, ketergantungan waktu, shared state, dan dependency eksternal.
Jika harus memilih prioritas, fokuslah pada test yang paling sering mengganggu merge pipeline dan paling merusak kepercayaan tim. CI yang dapat dipercaya bukan hasil dari retry tanpa batas, tetapi dari suite test yang deterministik, terisolasi, dan dirancang sesuai karakter sistem backend yang diuji.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!