Dalam beberapa waktu terakhir, kurasi tulisan developer di komunitas seperti DEV sering menyorot tema yang sama: kualitas software tidak lagi cukup dijaga dengan menjalankan semua test di satu lingkungan. Regresi modern sering muncul dari kombinasi kondisi yang berbeda—misalnya versi runtime tertentu, database yang berbeda, atau feature flag yang mengubah alur eksekusi. Di sinilah test matrix CI menjadi penting.
Masalahnya, banyak tim menerapkan matrix terlalu agresif: semua kombinasi dijalankan untuk semua jenis test, lalu pipeline menjadi lambat, mahal, dan penuh flaky. Solusi yang lebih efektif adalah merancang matrix berdasarkan risk coverage, memisahkan level test dengan jelas, dan menerapkan aturan quarantine serta retry yang disiplin. Dengan pendekatan ini, regresi tertangkap lebih awal tanpa membuat CI sulit dipercaya.
Mengapa test matrix CI perlu dirancang, bukan sekadar diperbanyak
Tujuan matrix bukan menjalankan semua kemungkinan kombinasi, melainkan menangkap kelas regresi yang paling mungkin dan paling mahal jika lolos ke produksi. Menambahkan lebih banyak axis memang meningkatkan cakupan, tetapi juga menaikkan:
- waktu eksekusi pipeline,
- biaya runner dan environment,
- jumlah kegagalan non-deterministik,
- beban debugging saat ada failure.
Karena itu, desain matrix yang baik selalu menjawab dua pertanyaan:
- Regresi apa yang ingin kita tangkap?
- Kombinasi minimum apa yang cukup untuk menangkapnya?
Contohnya, jika aplikasi hanya deploy di Linux, maka menguji semua integration test di tiga OS biasanya tidak sebanding nilainya. Sebaliknya, jika library Anda dipakai lintas platform oleh pengguna eksternal, pengujian lintas OS menjadi jauh lebih relevan.
Memilih dimensi matrix yang tepat
Empat dimensi yang paling sering memberi nilai nyata adalah OS, versi runtime, database, dan feature flag. Namun tiap dimensi tidak harus diterapkan ke semua test.
1. OS
Dimensi OS berguna ketika perilaku aplikasi dipengaruhi oleh filesystem, path separator, permission, shell command, binary dependency, atau packaging. Contoh kasus yang layak diuji lintas OS:
- CLI tool, SDK, atau developer tooling,
- aplikasi yang memanggil proses sistem,
- kode yang bergantung pada path/file handling,
- build frontend/native yang memakai toolchain berbeda per OS.
Untuk layanan backend yang hanya berjalan di container Linux, biasanya cukup:
- smoke test di beberapa OS bila distribusi artefak penting, atau
- semua test utama tetap di Linux untuk menjaga stabilitas dan kecepatan.
2. Versi runtime
Versi runtime sering menjadi sumber regresi karena perubahan perilaku standar library, performa parser, TLS stack, garbage collector, atau dependency compatibility. Pilihan umum adalah:
- versi minimum yang masih didukung,
- versi mayor yang paling banyak dipakai,
- versi terbaru yang ingin diadopsi lebih awal.
Ini memberi cakupan terhadap dua risiko sekaligus: kompatibilitas ke belakang dan kesiapan upgrade. Jangan menguji terlalu banyak versi jika tim tidak benar-benar mendukung semuanya secara resmi.
3. Database
Jika aplikasi mendukung lebih dari satu database, matrix database sangat bernilai karena perbedaan tidak hanya ada di syntax SQL, tetapi juga di:
- default isolation level,
- strictness constraint,
- tipe data dan casting,
- sorting/collation,
- perilaku transaction dan locking.
Namun tidak semua test perlu dijalankan di semua database. Unit test tidak mendapat banyak manfaat dari variasi ini. Yang paling relevan adalah integration test yang benar-benar menyentuh persistence layer.
4. Feature flag
Feature flag sering diabaikan dalam matrix, padahal banyak regresi muncul di jalur kode yang hanya aktif pada flag tertentu. Prinsipnya:
- uji default state yang sedang dipakai produksi,
- uji target state saat rollout sedang berlangsung,
- hindari kombinasi semua flag sekaligus jika interaksinya tidak relevan.
Jika ada banyak flag, kelompokkan berdasarkan risiko bisnis dan area kode yang terdampak. Tidak semua flag pantas menjadi axis matrix permanen.
Prioritas risk-based: pilih kombinasi yang paling bernilai
Cara paling aman untuk menghindari matrix yang meledak adalah memakai prioritas berbasis risiko. Buat daftar kombinasi, lalu nilai dengan kriteria seperti:
- Dampak pengguna: seberapa besar blast radius jika kombinasi ini gagal.
- Kemungkinan perubahan: apakah area ini sering disentuh.
- Riwayat bug: apakah kombinasi tersebut pernah menyebabkan insiden.
- Keragaman perilaku: apakah environment itu benar-benar berbeda.
- Biaya eksekusi: berapa lama dan mahal kombinasi ini dijalankan.
Dari situ, Anda bisa membagi kombinasi menjadi beberapa lapisan:
- Tier 1: wajib sebelum merge.
- Tier 2: dijalankan pada pull request tertentu, misalnya saat file tertentu berubah.
- Tier 3: dijalankan terjadwal, nightly, atau sebelum release.
Contoh praktis untuk layanan web:
- Pre-merge: Linux + runtime utama + database utama + flag default.
- Selective pre-merge: runtime minimum, database sekunder, atau flag rollout jika area terkait berubah.
- Nightly/release: kombinasi lintas runtime dan database yang lebih luas.
Aturan sederhananya: buat kombinasi mahal menjadi bersyarat, bukan default. CI yang selalu lambat akan mendorong tim mencari jalan pintas.
Pisahkan smoke, integration, dan end-to-end agar matrix tidak meledak
Kesalahan umum adalah menjalankan semua jenis test pada semua axis matrix. Padahal tiap level test punya tujuan dan kebutuhan cakupan berbeda.
Smoke test
Smoke test memverifikasi bahwa build dapat start, dependency utama terhubung, dan alur paling dasar tidak rusak. Test ini cocok dijalankan di matrix yang lebih lebar karena:
- durasi singkat,
- mudah diparalelkan,
- cepat memberi sinyal kompatibilitas dasar.
Contoh: aplikasi dapat boot, health check lulus, migration jalan, satu request utama sukses.
Integration test
Integration test memverifikasi interaksi komponen nyata: aplikasi dengan database, cache, queue, storage, atau service internal. Inilah area yang paling layak memakai dimensi database dan sebagian runtime.
Batasi cakupan ke kombinasi yang benar-benar punya perbedaan perilaku. Jika dua runtime tidak mengubah lapisan persistence secara signifikan, Anda tidak harus menggandakan semua integration test.
End-to-end test
E2E memberi keyakinan tinggi tetapi paling rentan flaky karena melibatkan browser, jaringan, timing, dan state environment. Karena itu:
- gunakan set E2E yang kecil untuk pre-merge,
- jalankan suite E2E yang lebih luas secara terjadwal atau sebelum release,
- hindari menjadikan E2E sebagai lapisan utama untuk semua kombinasi matrix.
Struktur yang umum dan sehat adalah:
- Unit test: tanpa matrix luas, fokus cepat dan deterministik.
- Smoke test: matrix lebih lebar.
- Integration test: matrix selektif berdasarkan database/runtime.
- E2E: kombinasi minimum sebelum merge, sisanya di nightly/release.
Contoh desain matrix yang realistis
Misalkan Anda mengelola API backend yang:
- deploy di Linux,
- mendukung dua versi runtime,
- menguji PostgreSQL sebagai database utama dan MySQL sebagai database sekunder,
- punya satu feature flag besar untuk jalur checkout baru.
Desain yang lebih sehat daripada menjalankan semua kombinasi:
- Smoke: Linux + 2 runtime + 2 state feature flag.
- Integration utama: Linux + runtime utama + PostgreSQL + default flag.
- Integration tambahan: Linux + runtime minimum + MySQL + default flag.
- Integration rollout: Linux + runtime utama + PostgreSQL + checkout flag aktif.
- E2E pre-merge: hanya kombinasi produksi paling representatif.
- Nightly: kombinasi tambahan yang tidak layak memblok merge.
Dengan pola ini, Anda tetap menangkap regresi penting tanpa mengalikan semua test secara buta.
Contoh YAML CI generik untuk test matrix CI
Berikut contoh YAML generik. Sintaks bisa disesuaikan ke GitHub Actions, GitLab CI, CircleCI, atau sistem lain, tetapi strukturnya menunjukkan prinsip yang sama: matrix per level test, kombinasi penting untuk merge, dan ekspansi terbatas untuk job terjadwal.
workflows:
pull_request:
jobs:
- job: unit
runs-on: linux
steps:
- checkout
- setup-runtime: "stable"
- run: ./scripts/test-unit.sh
- job: smoke_matrix
strategy:
matrix:
runtime: ["minimum", "stable"]
feature_flag_checkout_v2: ["off", "on"]
runs-on: linux
steps:
- checkout
- setup-runtime: ${{ matrix.runtime }}
- run: ./scripts/test-smoke.sh
env:
CHECKOUT_V2: ${{ matrix.feature_flag_checkout_v2 }}
- job: integration_primary
runs-on: linux
services:
db: postgres
steps:
- checkout
- setup-runtime: "stable"
- run: ./scripts/test-integration.sh
env:
DB_ENGINE: postgres
CHECKOUT_V2: off
- job: integration_secondary
if: changes_in(["db/**", "repository/**", "migrations/**"])
runs-on: linux
services:
db: mysql
steps:
- checkout
- setup-runtime: "minimum"
- run: ./scripts/test-integration.sh
env:
DB_ENGINE: mysql
CHECKOUT_V2: off
- job: integration_flagged_path
if: changes_in(["checkout/**", "feature-flags/**", "api/**"])
runs-on: linux
services:
db: postgres
steps:
- checkout
- setup-runtime: "stable"
- run: ./scripts/test-integration.sh
env:
DB_ENGINE: postgres
CHECKOUT_V2: on
- job: e2e_smoke
runs-on: linux
steps:
- checkout
- setup-runtime: "stable"
- run: ./scripts/test-e2e-smoke.sh
nightly:
jobs:
- job: extended_matrix
strategy:
matrix:
runtime: ["minimum", "stable"]
database: ["postgres", "mysql"]
feature_flag_checkout_v2: ["off", "on"]
runs-on: linux
steps:
- checkout
- setup-runtime: ${{ matrix.runtime }}
- start-database: ${{ matrix.database }}
- run: ./scripts/test-integration.sh
env:
DB_ENGINE: ${{ matrix.database }}
CHECKOUT_V2: ${{ matrix.feature_flag_checkout_v2 }}Beberapa hal penting dari contoh di atas:
- Unit test tidak dimasukkan ke matrix luas.
- Smoke test dipakai untuk memperluas cakupan runtime dan flag dengan biaya kecil.
- Integration test dipilih berdasarkan area perubahan dan risiko.
- E2E dibatasi pada smoke path.
- Nightly menangani kombinasi lengkap tanpa menghambat merge harian.
Strategi quarantine untuk flaky test
Flaky test adalah test yang kadang gagal tanpa perubahan kode relevan. Jika dibiarkan di jalur wajib merge, flaky test merusak kepercayaan tim terhadap CI. Solusi yang lebih sehat bukan mematikan test diam-diam, melainkan menerapkan quarantine dengan aturan yang jelas.
Kapan sebuah test masuk quarantine
- gagal intermiten dan tidak bisa direproduksi konsisten secara lokal,
- indikasi kuat kegagalan berasal dari timing, race, dependency eksternal, atau environment non-deterministik,
- sudah diverifikasi bukan bug produk yang nyata.
Aturan quarantine yang disarankan
- beri label atau marker khusus, misalnya
quarantined, - jangan jadikan hasilnya memblok merge,
- tetap jalankan dan laporkan hasilnya secara terpisah,
- wajib ada owner dan ticket perbaikan,
- tetapkan batas waktu; quarantine bukan status permanen.
Yang sering salah adalah memindahkan test ke quarantine lalu melupakannya. Quarantine hanya berguna jika tim memonitor jumlah test yang dikarantina dan tren kegagalannya.
Akar masalah flaky yang paling umum
- ketergantungan pada waktu nyata atau timezone,
- data bersama antar test,
- urutan eksekusi test yang memengaruhi state,
- assertion yang terlalu sensitif terhadap timing,
- dependency jaringan atau service eksternal yang tidak stabil,
- cleanup environment yang tidak tuntas.
Perbaikan jangka panjang hampir selalu melibatkan test isolation, kontrol clock, fixture yang deterministik, dan observability yang lebih baik pada test runner.
Aturan retry yang aman
Retry bisa membantu, tetapi juga berbahaya jika dipakai untuk menyembunyikan bug nyata. Aturan yang aman adalah:
- Jangan retry seluruh pipeline secara default.
- Retry hanya pada test atau langkah yang diketahui non-deterministik, misalnya startup browser atau wait kondisi asynchronous.
- Batasi jumlah retry; satu retry tambahan biasanya cukup untuk diagnosis awal.
- Catat first-failure dan retry-pass rate; jika sering lolos setelah retry, test itu tetap flaky.
- Jangan gunakan retry untuk unit test deterministik; jika unit test butuh retry, ada masalah desain test atau bug riil.
Retry yang aman biasanya lebih tepat di level infrastruktur atau harness, bukan sebagai pola umum pada assertion aplikasi. Misalnya, me-retry langkah menunggu service siap lebih masuk akal daripada me-retry seluruh skenario bisnis tanpa memahami penyebabnya.
Jika sebuah test sering merah lalu hijau setelah retry, anggap itu sinyal masalah kualitas test, bukan keberhasilan stabilisasi.
Workflow verifikasi sebelum merge
Pipeline yang sehat tidak hanya tentang job apa yang dijalankan, tetapi juga kapan dan dalam urutan apa. Workflow verifikasi sebelum merge sebaiknya bertingkat:
1. Fast feedback awal
- linting, static analysis, dan unit test,
- smoke matrix yang cepat,
- durasi dijaga singkat agar developer cepat mendapat sinyal.
2. Validasi risiko utama
- integration test pada kombinasi produksi paling representatif,
- job tambahan yang aktif berdasarkan area perubahan,
- optional manual approval hanya untuk perubahan berisiko tinggi.
3. Merge gate yang jelas
Tentukan job mana yang benar-benar wajib hijau sebelum merge. Hindari situasi di mana puluhan job status campuran membuat keputusan merge tidak jelas. Beberapa prinsip yang berguna:
- status wajib dibatasi pada sinyal berkualitas tinggi,
- job experimental atau quarantine tidak memblok merge,
- nightly failure harus masuk ke backlog dan triage rutin, bukan diabaikan.
4. Verifikasi pasca-merge
Tidak semua kombinasi layak dijalankan sebelum merge. Karena itu, jalankan matrix yang lebih luas setelah merge ke branch utama atau secara terjadwal. Kuncinya adalah respons cepat jika ada kegagalan:
- notifikasi ke owner area,
- auto-create issue jika perlu,
- kebijakan revert atau freeze bila kombinasi penting gagal terus.
Metrik yang perlu dipantau
Tanpa metrik, sulit menilai apakah test matrix CI benar-benar membantu atau justru memperburuk alur delivery. Metrik yang praktis dipantau:
- Failure rate per job dan per kombinasi matrix: kombinasi mana yang paling sering gagal.
- Flake rate: proporsi test yang gagal lalu lolos saat diulang.
- Time to signal: berapa cepat developer tahu PR bermasalah.
- Queue time dan total pipeline duration: apakah matrix membuat feedback terlambat.
- Merge-blocking false positive: kegagalan CI yang bukan bug produk.
- Defect escape rate: regresi yang lolos ke staging/produksi meski CI hijau.
- Quarantine count dan age: jumlah test yang dikarantina dan berapa lama dibiarkan.
Metrik paling berguna bukan yang paling banyak, tetapi yang bisa dipakai untuk keputusan konkret. Misalnya, jika satu kombinasi nightly hampir tidak pernah menangkap bug dan sering gagal karena environment, mungkin kombinasi itu lebih baik dipangkas atau didesain ulang.
Checklist implementasi
- Identifikasi environment yang benar-benar didukung: OS, runtime, database, dan flag penting.
- Kelompokkan test menjadi unit, smoke, integration, dan E2E.
- Tentukan kombinasi wajib merge berdasarkan risiko tertinggi.
- Buat aturan selective execution berdasarkan file atau area perubahan.
- Pindahkan kombinasi mahal ke nightly atau pre-release.
- Terapkan quarantine dengan owner, ticket, dan SLA perbaikan.
- Batasi retry hanya pada titik non-deterministik yang jelas.
- Pastikan environment test reproducible: seed data, clock, cleanup, isolation.
- Pantau metrik flake rate, durasi, dan defect escape.
- Tinjau matrix secara berkala; hapus kombinasi yang tidak memberi nilai.
Anti-pattern yang sering terjadi
1. Cartesian explosion tanpa alasan
Semua OS dikali semua runtime dikali semua database dikali semua flag untuk semua test. Hasilnya mahal dan sulit dipelihara, tetapi tidak selalu lebih aman.
2. Semua test dijadikan merge gate
Jika setiap job memblok merge, developer akan menunggu terlalu lama dan kepercayaan ke CI turun ketika false positive meningkat.
3. Retry sebagai solusi universal
Retry yang membabi buta hanya menutupi flaky, tidak memperbaikinya.
4. Feature flag diabaikan
Rollout path sering justru paling berisiko. Jika CI hanya menguji default path, regresi mudah lolos saat flag diaktifkan.
5. Database sekunder diklaim didukung, tapi jarang diuji
Dukungan resmi tanpa test integration yang relevan hampir selalu berujung bug saat pengguna nyata memakainya.
6. Quarantine tanpa tindak lanjut
Ini hanya cara lain untuk menurunkan kualitas suite sambil mempertahankan ilusi cakupan.
7. E2E dijadikan pengganti integration test
E2E terlalu mahal dan rapuh untuk memikul seluruh beban validasi. Banyak bug lebih murah ditangkap di integration test yang terarah.
Penutup
Test matrix CI yang efektif bukan yang paling besar, tetapi yang paling tepat sasaran. Fokuskan matrix pada dimensi yang benar-benar memengaruhi perilaku aplikasi—OS, runtime, database, dan feature flag—lalu petakan ke level test yang sesuai. Gunakan prioritas berbasis risiko untuk menentukan kombinasi wajib merge, perluas cakupan lewat smoke dan nightly, dan perlakukan flaky test sebagai masalah engineering yang harus diisolasi serta diperbaiki.
Jika pipeline Anda sering lambat atau tidak dipercaya, biasanya masalahnya bukan kurang banyak test, melainkan desain matrix yang kurang disiplin. Dengan matrix yang kurasi kombinasinya baik, aturan quarantine yang tegas, dan metrik yang dipantau rutin, regresi bisa tertangkap lebih awal tanpa memperbanyak flaky.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!