Strategi test cepat untuk cegah regresi tanpa flaky di CI berangkat dari satu prinsip sederhana: feedback yang cepat membuat developer lebih mudah memperbaiki masalah sebelum perubahan kecil berubah menjadi bug besar. Namun test yang cepat saja tidak cukup. Jika hasilnya sering berubah-ubah, gagal karena kondisi lingkungan, atau perlu di-retry berkali-kali, maka CI kehilangan fungsi utamanya sebagai sinyal kualitas yang dapat dipercaya.

Solusinya bukan sekadar menambah lebih banyak test, melainkan menata jenis test, ruang lingkup, dan target waktunya. Dengan membangun test pyramid yang sehat, membatasi E2E pada alur paling kritis, memisahkan smoke, regression, dan full suite, serta mengendalikan sumber flaky seperti waktu, network, randomness, dan shared state, tim bisa menjaga software tetap cepat sekaligus andal.

Mengapa kecepatan test penting, tetapi tidak boleh dibeli dengan ketidakstabilan

Test suite yang lambat punya biaya nyata:

  • Developer menunda menjalankan test lokal.
  • Pull request menumpuk karena antrean CI panjang.
  • Bug regresi baru diketahui terlambat.
  • Tim mulai menoleransi kegagalan CI karena dianggap “pasti flaky”.

Di sisi lain, test suite yang terlalu agresif dipercepat tanpa desain yang baik juga bermasalah. Misalnya, semua dependency dimock berlebihan sampai test tidak lagi memverifikasi integrasi penting, atau test paralel dijalankan di atas resource bersama sehingga hasilnya acak.

Tujuan yang lebih tepat adalah: memberikan feedback tercepat yang masih cukup akurat untuk keputusan engineering. Artinya, tidak semua test harus berjalan di setiap event CI, dan tidak semua skenario perlu divalidasi lewat browser end-to-end.

Trade-off: test suite cepat vs cakupan

Tidak ada suite yang sekaligus paling cepat, paling murah dirawat, dan memiliki cakupan penuh di semua lapisan. Karena itu, strategi yang sehat perlu menerima trade-off berikut:

1. Test cepat biasanya lebih sempit ruang lingkupnya

Unit test cenderung cepat karena hanya memverifikasi fungsi atau komponen kecil. Kelemahannya, ia tidak otomatis menangkap kegagalan wiring, kontrak API, query database, atau konfigurasi runtime.

2. Test dengan cakupan besar biasanya lebih lambat dan lebih rapuh

End-to-end test memverifikasi sistem lebih utuh, tetapi mahal dijalankan dan dirawat. Semakin banyak komponen yang dilibatkan, semakin besar peluang flaky akibat timing, dependency eksternal, atau data yang tidak konsisten.

3. Cakupan tinggi tidak selalu berarti perlindungan tinggi

Banyak tim memiliki banyak test, tetapi salah distribusi. Misalnya, terlalu banyak E2E untuk memverifikasi hal yang sebenarnya bisa dijamin oleh unit atau integration test. Hasilnya: pipeline lambat, debugging sulit, dan sinyal kualitas justru lemah.

Prinsip praktisnya: letakkan sebagian besar verifikasi pada lapisan termurah dan paling deterministik, lalu gunakan lapisan yang lebih mahal hanya untuk risiko yang memang tidak bisa dibuktikan di bawahnya.

Menyusun test pyramid yang sehat

Model yang paling berguna untuk topik ini tetap test pyramid. Bukan karena harus diikuti secara kaku, tetapi karena ia memaksa tim berpikir tentang distribusi biaya dan nilai.

Lapisan 1: Unit test sebagai fondasi utama

Gunakan unit test untuk memverifikasi:

  • Logika bisnis murni.
  • Validasi input dan transformasi data.
  • Aturan perhitungan, mapping, permission, atau branching logic.
  • Edge case yang sulit dipicu dari UI.

Ciri unit test yang baik:

  • Tidak bergantung pada network nyata.
  • Tidak menunggu waktu nyata.
  • Tidak membaca state global yang berubah-ubah.
  • Dapat dijalankan ribuan kali dengan hasil konsisten.

Lapisan 2: Integration test untuk kontrak antar komponen

Integration test cocok untuk memverifikasi:

  • Repository atau query terhadap database.
  • Interaksi service dengan cache, queue, atau storage.
  • Kontrak HTTP internal atau adaptasi ke service lain.
  • Serialisasi, deserialisasi, dan skema payload.

Integration test sebaiknya masih terisolasi dari dependency eksternal yang tidak Anda kontrol. Contohnya, lebih baik menggunakan database test yang disposable daripada database shared lintas job. Untuk layanan pihak ketiga, gunakan stub atau mock server yang memverifikasi kontrak request/response.

Lapisan 3: E2E test secukupnya, bukan sebanyak mungkin

E2E test penting untuk memverifikasi alur kritis yang melintasi banyak lapisan, misalnya:

  • Login pengguna.
  • Checkout atau pembayaran sebelum integrasi final.
  • Pembuatan order atau submit form utama.
  • Alur permission yang sensitif.

Namun jumlahnya perlu dibatasi. Jika suatu skenario sudah divalidasi dengan baik di unit dan integration test, E2E tidak perlu mengulang semua variasi edge case. E2E dipakai untuk menjawab pertanyaan: apakah sistem utama benar-benar tersambung dan berfungsi dari sudut pandang pengguna?

Aturan praktis: simpan E2E hanya untuk alur bisnis yang kritis, berisiko tinggi, atau pernah menimbulkan insiden. Jangan menjadikan E2E sebagai pengganti test design di lapisan bawah.

Memisahkan smoke, regression, dan full suite

Salah satu penyebab CI lambat adalah semua test dijalankan di semua tahap. Padahal kebutuhan verifikasi sebelum merge tidak sama dengan sebelum rilis.

Smoke suite

Smoke suite adalah subset kecil yang menjawab: apakah perubahan ini merusak jalur utama?

Biasanya berisi:

  • Lint dan static analysis dasar.
  • Unit test prioritas tinggi.
  • Beberapa integration test inti.
  • 1-5 E2E untuk happy path paling kritis.

Smoke suite cocok untuk:

  • Pull request.
  • Push ke branch aktif.
  • Pre-merge gate yang menuntut feedback cepat.

Regression suite

Regression suite lebih luas. Isinya mencakup:

  • Seluruh unit test.
  • Mayoritas integration test.
  • E2E yang dipilih berdasarkan risiko.
  • Test untuk bug yang pernah terjadi.

Regression suite cocok untuk:

  • Merge ke branch utama.
  • Nightly build.
  • Validasi kandidat rilis.

Full suite

Full suite digunakan untuk pemeriksaan paling lengkap, misalnya:

  • Kombinasi environment tertentu.
  • Compatibility matrix yang lebih luas.
  • Load/smoke tambahan terhadap komponen tertentu.
  • Skema migrasi database atau recovery test.

Full suite umumnya tidak perlu memblokir setiap commit jika biayanya tinggi. Jalankan pada jadwal tertentu, sebelum rilis besar, atau saat perubahan menyentuh area sensitif.

Contoh matriks jenis test

+----------------+----------------------+----------------------+---------------------------+
| Jenis Test     | Tujuan               | Kapan Dijalankan     | Batas Waktu Disarankan    |
+----------------+----------------------+----------------------+---------------------------+
| Lint/Static    | Error cepat, style   | Setiap PR/push       | Sangat singkat            |
| Unit           | Logika lokal         | Setiap PR/push       | Singkat                   |
| Integration    | Kontrak komponen     | PR penting / merge   | Menengah                  |
| Smoke E2E      | Happy path kritis    | Sebelum merge        | Singkat-menengah          |
| Regression     | Cegah bug berulang   | Merge / nightly      | Menengah                  |
| Full Suite     | Validasi lengkap     | Sebelum rilis        | Lebih panjang, terjadwal  |
+----------------+----------------------+----------------------+---------------------------+

Angka pastinya berbeda di tiap tim. Yang penting adalah setiap pipeline punya tujuan dan budget waktu yang jelas.

Menetapkan test budget per pipeline

Tanpa budget, suite hampir selalu membesar pelan-pelan sampai CI terasa berat. Budget memaksa tim berpikir apakah sebuah test layak dimasukkan ke jalur cepat atau dipindahkan ke suite lain.

Cara menetapkan budget

  1. Tentukan jenis pipeline: PR, merge ke main, nightly, pre-release.
  2. Tetapkan target durasi total untuk tiap pipeline.
  3. Bagi budget itu per kategori: lint, unit, integration, E2E.
  4. Monitor tren durasi, bukan hanya durasi sesaat.
  5. Tolak penambahan test mahal jika tidak ada penghapusan atau optimasi setara.

Contoh pendekatan:

  • PR pipeline: hanya test yang memberi sinyal cepat untuk keputusan merge.
  • Main branch pipeline: validasi lebih luas karena branch ini akan dipakai tim lain.
  • Pre-release pipeline: boleh lebih berat karena frekuensinya lebih rendah dan nilai bisnisnya lebih tinggi.

Budget bukan berarti mengejar waktu serendah mungkin

Kesalahan umum adalah memangkas test penting demi mengejar angka durasi. Budget seharusnya menjadi alat prioritas, bukan sekadar target kosmetik. Jika ada alur bisnis berisiko tinggi, lebih baik simpan test tersebut dan optimalkan cara menjalankannya: paralelisasi, test data yang lebih efisien, setup environment yang lebih ringan, atau caching dependency build.

Penyebab flaky test dan cara menghilangkannya

Flaky test adalah test yang kadang lulus, kadang gagal, tanpa perubahan kode yang relevan. Ini salah satu sumber kebisingan terbesar di CI.

1. Shared state antar test

Masalah umum:

  • Database tidak dibersihkan dengan benar.
  • Cache masih berisi data dari test sebelumnya.
  • File temporary atau object storage dipakai bersama.
  • Urutan eksekusi memengaruhi hasil.

Perbaikan:

  • Buat setiap test independen.
  • Reset state sebelum atau sesudah test.
  • Gunakan namespace unik untuk resource test.
  • Hindari asumsi bahwa test tertentu berjalan lebih dulu.

2. Ketergantungan pada waktu nyata

Test sering gagal karena memeriksa timeout, token expiry, scheduler, atau timestamp dengan selisih waktu sangat kecil.

Perbaikan:

  • Gunakan fake clock atau abstraction untuk waktu.
  • Bandingkan rentang waktu jika memang perlu, bukan nilai real-time mentah.
  • Jangan bergantung pada sleep tetap untuk menunggu kondisi asynchronous.
// Contoh pseudo-code: injeksikan clock agar test deterministik
interface Clock {
  now(): Date
}

class FixedClock implements Clock {
  constructor(private current: Date) {}
  now(): Date { return this.current }
}

class TokenService {
  constructor(private clock: Clock) {}

  isExpired(expiry: Date): boolean {
    return this.clock.now().getTime() > expiry.getTime()
  }
}

3. Ketergantungan pada network atau layanan eksternal

Network nyata menambah latency, variasi respons, rate limit, dan kegagalan di luar kontrol Anda. Untuk sebagian besar test CI, itu sumber flaky yang tidak perlu.

Perbaikan:

  • Mock atau stub layanan eksternal di unit test.
  • Gunakan mock server untuk integration test kontrak.
  • Batasi test ke sandbox eksternal hanya pada pipeline khusus atau verifikasi rilis.

4. Randomness tanpa kontrol

ID acak, urutan data acak, atau generator data acak sering membuat asersi tidak stabil.

Perbaikan:

  • Gunakan seed tetap bila memungkinkan.
  • Simpan input acak yang menyebabkan kegagalan agar bisa direproduksi.
  • Jangan mengasumsikan urutan hasil jika sistem tidak menjaminnya.

5. Asynchronous wait yang rapuh

Polling yang terlalu singkat, sleep hard-coded, atau asumsi event akan selesai dalam waktu tertentu sering memicu flaky.

Perbaikan:

  • Tunggu berdasarkan kondisi, bukan durasi tetap.
  • Tetapkan timeout yang masuk akal dan observabel.
  • Pastikan queue, job worker, atau background task memiliki sinkronisasi yang jelas untuk mode test.

Teknik isolasi dependency yang benar

Isolasi bukan berarti memock semua hal. Tujuannya adalah membatasi faktor eksternal agar test tetap deterministik sambil tetap memverifikasi perilaku yang penting.

Kapan memakai mock

  • Untuk dependency yang lambat atau tidak stabil.
  • Untuk memverifikasi interaksi, misalnya apakah request dikirim dengan payload benar.
  • Untuk mensimulasikan error path yang sulit dipicu di environment nyata.

Kapan memakai fake atau in-memory implementation

  • Untuk dependency yang punya perilaku sederhana dan bisa ditiru dengan aman.
  • Untuk test yang butuh eksekusi cepat tanpa biaya I/O besar.

Trade-off-nya: implementasi fake bisa menyimpang dari sistem nyata jika tidak dirawat. Karena itu tetap butuh lapisan integration test terhadap adapter sebenarnya.

Kapan memakai dependency nyata

  • Ketika nilai utama test adalah memverifikasi integrasi aktual.
  • Ketika bug historis sering muncul pada wiring, konfigurasi, atau query.
  • Ketika perilaku dependency terlalu kompleks untuk dimock secara aman.

Pendekatan umum yang sehat adalah:

  • Unit test: mock/fake seperlunya.
  • Integration test: dependency internal nyata, eksternal distub.
  • E2E: sistem utama nyata, integrasi pihak ketiga dibatasi sesuai kebutuhan risiko.

Retry yang aman: kapan boleh, kapan berbahaya

Retry sering dipakai untuk “menstabilkan” CI, tetapi jika diterapkan sembarangan ia hanya menyembunyikan flaky, bukan memperbaikinya.

Retry yang berbahaya

  • Me-retry semua test tanpa klasifikasi.
  • Menganggap test lulus jika salah satu percobaan berhasil.
  • Tidak mencatat bahwa test sempat gagal.

Ini berbahaya karena tim kehilangan sinyal bahwa ada nondeterminisme.

Retry yang relatif aman

  • Retry terbatas hanya untuk langkah infrastruktur yang memang rawan transien, seperti unduhan dependency atau startup service test.
  • Retry pada assertion berbasis eventual consistency, tetapi hanya jika sistem memang dirancang eventual dan kondisi tunggunya jelas.
  • Menandai test sebagai flaky dan membuka tiket perbaikan, bukan membiarkannya permanen.

Prinsip penting: retry hanya boleh dipakai untuk kegagalan transien yang dipahami penyebabnya. Jika sumber masalah belum jelas, prioritaskan reproduksi dan perbaikan akar masalah.

Workflow verifikasi sebelum merge dan sebelum rilis

Sebelum merge

Target tahap ini adalah feedback cepat dan kepercayaan tinggi bahwa perubahan aman digabungkan.

  1. Jalankan lint dan static analysis.
  2. Jalankan unit test yang relevan atau seluruh unit test jika masih cepat.
  3. Jalankan integration test untuk area yang disentuh.
  4. Jalankan smoke E2E pada happy path kritis.
  5. Tolak merge jika ada flaky yang belum dikarantina.

Jika codebase besar, gunakan seleksi test berbasis perubahan dengan hati-hati. Ini berguna untuk mempercepat PR, tetapi jangan jadikan satu-satunya lapisan perlindungan. Tetap sediakan regression suite yang berjalan setelah merge atau secara berkala.

Sebelum rilis

Target tahap ini berbeda: bukan sekadar cepat, tetapi mengurangi risiko operasional.

  1. Jalankan regression suite penuh.
  2. Jalankan migrasi database pada environment yang menyerupai produksi.
  3. Verifikasi alur rollback atau compatibility jika ada perubahan skema.
  4. Jalankan E2E tambahan untuk fitur yang berubah signifikan.
  5. Pastikan tidak ada flaky yang diabaikan di area rilis.

Untuk sistem dengan banyak integrasi, tahap pre-release sering menjadi tempat yang tepat untuk memvalidasi dependency eksternal nyata dalam skop terbatas, bukan di setiap commit.

Checklist CI untuk test cepat dan stabil

  • Setiap pipeline punya tujuan dan budget durasi yang jelas.
  • Unit, integration, smoke, regression, dan full suite dipisahkan secara eksplisit.
  • E2E dibatasi pada alur kritis, bukan seluruh variasi kasus.
  • Test independen dan tidak bergantung pada urutan eksekusi.
  • Database, cache, file, dan message broker dibersihkan atau diisolasi per job.
  • Waktu dikendalikan melalui abstraction atau fake clock.
  • Network eksternal tidak dipakai di jalur CI cepat kecuali benar-benar perlu.
  • Randomness diberi seed atau dibuat dapat direproduksi.
  • Asynchronous wait berbasis kondisi, bukan sleep tetap.
  • Retry hanya dipakai untuk kegagalan transien yang dipahami.
  • Test flaky ditandai, diukur, dan diprioritaskan untuk diperbaiki.
  • Durasi tiap kategori test dipantau dari waktu ke waktu.
  • Penambahan test mahal harus disertai justifikasi risiko atau optimasi lain.

Langkah audit saat suite mulai melambat

Jika CI terasa makin berat, jangan langsung menambah mesin atau runner. Audit dulu untuk mengetahui sumber perlambatan yang sebenarnya.

1. Profilkan durasi per kategori dan per test

Kumpulkan data:

  • Test paling lambat.
  • Setup environment paling mahal.
  • Job yang paling sering antre.
  • Rasio waktu eksekusi test vs bootstrap.

Sering kali masalah terbesar bukan pada logika test, tetapi pada setup yang diulang-ulang secara tidak perlu.

2. Cari duplikasi verifikasi

Tanyakan:

  • Apakah skenario yang sama diuji di unit, integration, dan E2E tanpa nilai tambahan?
  • Apakah E2E memverifikasi edge case yang lebih cocok di unit test?
  • Apakah ada test warisan untuk fitur yang sudah dihapus atau diubah total?

3. Identifikasi flaky terselubung

Test yang kadang timeout atau sering di-retry biasanya juga menyumbang waktu besar. Kategorikan terpisah dan perbaiki lebih dulu.

4. Optimalkan setup dan fixture

  • Kurangi bootstrap aplikasi penuh jika tidak diperlukan.
  • Gunakan fixture minimal yang relevan dengan skenario.
  • Hindari data test besar untuk kasus sederhana.
  • Pertimbangkan paralelisasi jika resource benar-benar terisolasi.

5. Pindahkan test ke lapisan yang lebih murah jika memungkinkan

Jika sebuah bug bisa dicegah oleh unit test deterministik, tidak perlu menunggu E2E panjang untuk menemukannya. Ini salah satu cara paling efektif mengembalikan kecepatan CI tanpa menurunkan perlindungan.

Kesalahan umum yang sering terjadi

  • Terlalu banyak E2E: suite terlihat meyakinkan, tetapi lambat dan rapuh.
  • Mock berlebihan: test cepat tetapi gagal menangkap integrasi nyata.
  • Retry sebagai solusi utama: flaky disembunyikan, bukan diperbaiki.
  • Tidak ada budget: suite membesar tanpa kontrol.
  • Tidak memisahkan pipeline: semua test dijalankan di semua tahap.
  • Tidak mengukur tren: tim baru sadar setelah CI sudah terlalu lambat.

Penutup

Strategi test cepat untuk cegah regresi tanpa flaky di CI bukan tentang mengejar jumlah test terbanyak atau waktu eksekusi terpendek. Intinya adalah membangun sinyal kualitas yang cepat, tepat, dan dapat dipercaya. Caranya: distribusikan verifikasi dengan test pyramid, batasi E2E pada alur paling kritis, pisahkan smoke, regression, dan full suite, lalu tetapkan budget per pipeline agar pertumbuhan suite tetap terkendali.

Begitu fondasi itu ada, flaky test akan lebih mudah dideteksi dan dihapus karena sumber nondeterminismenya terlihat jelas: shared state, waktu, network, randomness, atau async wait yang rapuh. Hasil akhirnya bukan hanya CI yang lebih cepat, tetapi workflow engineering yang lebih tenang: developer percaya pada hasil test, merge lebih lancar, dan risiko regresi turun tanpa harus membayar dengan pipeline yang melelahkan.