Mengurangi flaky test saat migrasi dari Vagrant ke container lokal bukan soal menambah retry secara membabi buta. Masalah utamanya biasanya muncul karena asumsi test lama diam-diam bergantung pada karakteristik environment Vagrant: filesystem yang berbeda, startup service yang lebih lambat namun stabil, hostname tertentu, timezone yang sudah cocok, atau isolasi yang lebih longgar.

Ketika tim pindah ke container lokal yang lebih ringan, banyak test mulai gagal secara acak walaupun kode aplikasi tidak berubah. Solusinya adalah mengidentifikasi sumber nondeterminism, lalu membangun workflow verifikasi yang memastikan hasil test konsisten di laptop developer dan di CI.

Mengapa migrasi ke container lokal memunculkan flaky test

Vagrant dan container sama-sama dipakai untuk menyamakan environment, tetapi model eksekusinya berbeda. Vagrant biasanya menjalankan VM yang lebih mirip mesin penuh, sedangkan container berbagi kernel host dan sangat bergantung pada konfigurasi mount, jaringan, serta urutan startup service.

Akibatnya, test yang tampak stabil di Vagrant bisa mulai flake di container karena:

  • Filesystem behavior berubah, terutama pada bind mount antara host dan container.
  • Waktu startup dependency berbeda, sehingga aplikasi mencoba terkoneksi terlalu cepat ke database, cache, atau broker.
  • Clock dan timezone tidak konsisten antara host, container aplikasi, dan service lain.
  • Network alias atau hostname berubah, terutama jika test diam-diam mengandalkan nama host tertentu.
  • Volume mounting memengaruhi permission, caching, atau file watch.
  • Isolation lebih ketat atau justru lebih longgar, sehingga race condition yang sebelumnya tersembunyi menjadi terlihat.

Poin pentingnya: flaky test setelah migrasi bukan berarti container buruk. Biasanya ini berarti test atau aplikasi memiliki asumsi environment yang belum dieksplisitkan.

Sumber flaky test yang paling sering muncul

1. Perbedaan filesystem dan bind mount

Pada Vagrant, sinkronisasi file sering berjalan dengan karakteristik tertentu. Di container lokal, bind mount dari host ke container bisa memiliki latensi metadata, permission yang berbeda, atau event file yang tidak selalu identik. Ini sering memukul:

  • test yang menunggu file muncul segera setelah ditulis,
  • proses build yang membaca file sebelum flush selesai,
  • watcher yang bergantung pada event filesystem,
  • test cleanup yang menghapus file tetapi file handle masih terbuka.

Gejala umum: test kadang gagal dengan pesan file tidak ditemukan, file masih terkunci, snapshot belum ter-update, atau hasil kompilasi belum tersedia saat assertion dijalankan.

Strategi perbaikan:

  • Jangan mengasumsikan operasi file langsung terlihat oleh proses lain tanpa sinkronisasi.
  • Untuk test, lebih aman menggunakan direktori temporary di dalam container dibanding mengandalkan bind mount host jika memungkinkan.
  • Pastikan cleanup dilakukan eksplisit dan proses penulis file benar-benar selesai sebelum assertion.
  • Kurangi test yang bergantung pada file watcher untuk jalur kritis CI.

2. Timing startup service dan race condition

Ini salah satu penyebab terbesar. Container database bisa berstatus running, tetapi belum siap menerima koneksi. Hal yang sama berlaku untuk Redis, search engine, message broker, atau mock service.

Kesalahan umum: pipeline hanya menjalankan container up lalu langsung mengeksekusi test. Status process hidup tidak sama dengan service siap.

Yang perlu dilakukan:

  • Tambahkan health check dependency yang benar-benar memverifikasi readiness, bukan hanya port terbuka.
  • Bedakan startup aplikasi dari readiness dependency.
  • Pastikan migrasi database, seed data, dan warm-up cache tidak dijalankan paralel tanpa kontrol urutan.

Contoh pendekatan shell yang lebih aman:

#!/usr/bin/env sh
set -eu

wait_for_db() {
  max=30
  i=0
  until nc -z db 5432; do
    i=$((i+1))
    if [ "$i" -ge "$max" ]; then
      echo "database tidak siap" >&2
      exit 1
    fi
    sleep 1
  done
}

wait_for_db
./bin/run-migrations
./bin/run-tests

Contoh di atas masih minimal. Untuk service yang butuh readiness lebih akurat, gunakan pemeriksaan yang benar-benar mencoba koneksi atau query ringan, bukan hanya memeriksa port.

3. Clock, timezone, dan ketergantungan pada waktu

Test yang melibatkan expiry token, job scheduler, timestamp database, atau format tanggal sangat sensitif terhadap perbedaan waktu. Di Vagrant, timezone mungkin sudah tetap. Di container, sebagian image memakai UTC, sementara host developer memakai timezone lokal.

Gejala umum:

  • assertion tanggal gagal hanya di mesin tertentu,
  • job dianggap belum jatuh tempo di satu environment tetapi sudah jatuh tempo di yang lain,
  • test yang melewati pergantian menit atau detik gagal sporadis.

Strategi perbaikan:

  • Standarkan timezone test, umumnya UTC.
  • Gunakan fake clock atau abstraction waktu di test integration bila memungkinkan.
  • Hindari assertion yang membandingkan timestamp secara terlalu presisi jika tidak perlu.
  • Log timezone dan waktu sistem pada awal test run untuk mempermudah diagnosis.

Jika test lulus saat dijalankan sendiri tetapi gagal saat suite penuh berjalan lebih lama, periksa dependency terhadap waktu, TTL cache, atau token expiry.

4. Network alias, DNS internal, dan hostname

Di Vagrant, aplikasi mungkin terbiasa mengakses service lewat hostname seperti localhost, 127.0.0.1, atau nama private tertentu. Di container, localhost berarti container itu sendiri, bukan host atau service lain.

Kesalahan yang sering terjadi:

  • Aplikasi di dalam container mencoba mengakses database di localhost.
  • Test mengandalkan alias yang ada di Vagrant tetapi tidak ada di network container.
  • Service URL di-hardcode berbeda antara lokal dan CI.

Praktik yang lebih baik:

  • Gunakan nama service yang konsisten di network container, misalnya db, redis, api-mock.
  • Jangan hardcode hostname environment-spesifik di test.
  • Pastikan konfigurasi test membaca endpoint dari environment variable yang seragam di lokal dan CI.

5. Volume mounting, permission, dan user mismatch

Masalah permission sering muncul saat process di container berjalan sebagai user berbeda dari pemilik file di host. Ini bisa menyebabkan cache test tidak bisa ditulis, artifact tidak bisa dihapus, atau migrasi gagal membuat file sementara.

Gejala umum: test kadang gagal dengan error permission denied, file generated tidak bisa dibersihkan, atau hasil test berbeda tergantung siapa yang menjalankan container.

Yang perlu dicek:

  • Direktori cache, log, temporary file, dan artifact test.
  • User/group yang menjalankan process test.
  • Apakah hasil build ditulis ke bind mount host atau ke volume internal container.

6. Isolation dan kebocoran state

Container sering memberi isolasi lebih jelas, sehingga shared state yang dulu tersembunyi jadi terlihat. Sebaliknya, jika Anda memakai volume yang persisten atau service bersama antar test, state lama bisa bocor ke run berikutnya.

Contoh:

  • database test tidak di-reset penuh,
  • queue masih berisi pesan dari run sebelumnya,
  • cache key tidak memakai namespace per test suite,
  • port bentrok karena test paralel memakai resource global yang sama.

Flaky test akibat state leak sering terlihat acak, tetapi pola aslinya biasanya terkait urutan eksekusi test.

Strategi testing yang lebih tahan terhadap perubahan environment

Mulai dari smoke test yang cepat dan deterministik

Sebelum menjalankan suite integration yang mahal, jalankan smoke test yang memverifikasi jalur dasar:

  • container aplikasi hidup,
  • dependency utama siap,
  • migrasi berhasil,
  • satu endpoint atau satu use case utama berjalan.

Smoke test berguna untuk memisahkan kegagalan bootstrap environment dari kegagalan logika aplikasi. Jika smoke test sudah gagal, jangan langsung menyalahkan test business flow yang lebih dalam.

Gunakan integration test yang eksplisit terhadap dependency nyata

Setelah smoke test, baru jalankan integration test terhadap database, cache, queue, atau service mock. Pastikan test ini mendeklarasikan dependency secara jelas dan tidak bergantung pada urutan suite lain.

Prinsip yang membantu:

  • Setiap suite tahu dependency apa yang wajib tersedia.
  • Data awal diketahui dan konsisten.
  • Cleanup dilakukan eksplisit.
  • Assertion fokus pada perilaku yang penting, bukan detail timing yang tidak relevan.

Pakai seed data yang tetap

Seed data tetap adalah fondasi penting untuk mengurangi flaky test. Jika fixture berubah-ubah tergantung waktu, urutan insert, atau data dari host, hasil test menjadi sulit direproduksi.

Praktik yang disarankan:

  • Gunakan seed minimal yang mewakili kasus uji utama.
  • Hindari data acak kecuali nilainya dicatat dan dapat direproduksi.
  • Reset database ke state yang diketahui sebelum suite berjalan.
  • Jika perlu parallel test, gunakan database terpisah atau namespace data terpisah.

Data acak boleh dipakai untuk menemukan edge case, tetapi bukan sebagai fondasi test stabil harian.

Tambahkan health check dependency, bukan sleep statis

sleep 10 sering tampak menyelesaikan masalah sementara, tetapi ini anti-pattern. Durasi yang cukup di laptop A belum tentu cukup di CI, dan durasi yang aman biasanya terlalu lama untuk workflow harian.

Lebih baik gunakan health check terukur:

  • cek koneksi database dengan query sederhana,
  • cek cache dengan operasi ping atau set/get ringan,
  • cek service HTTP dengan endpoint readiness yang relevan.

Tujuannya bukan sekadar menunggu, tetapi memastikan dependency siap untuk skenario test Anda.

Retry yang terukur, bukan menutupi bug

Retry kadang berguna, tetapi hanya untuk error yang memang transien, misalnya kegagalan koneksi singkat saat startup dependency. Retry tidak boleh dipakai untuk menutupi race condition aplikasi atau assertion yang salah.

Aturan praktis:

  • Retry hanya pada langkah bootstrap atau operasi yang memang bisa transient.
  • Batasi jumlah retry dan intervalnya.
  • Log alasan retry secara eksplisit.
  • Jika retry sering terpakai, anggap itu sinyal bug yang belum diperbaiki.

Retry yang baik mengurangi noise. Retry yang buruk menyembunyikan kerusakan desain test.

Tingkatkan observability log test

Saat test flake, masalah terbesar biasanya bukan kegagalan itu sendiri, melainkan kurangnya bukti. Karena itu, observability untuk test environment sangat penting.

Minimal, simpan:

  • log aplikasi,
  • log container dependency,
  • timestamp awal/akhir suite,
  • environment summary: timezone, hostname, endpoint service, variabel konfigurasi penting,
  • hasil health check sebelum test dimulai.

Jika memungkinkan, kelompokkan log per suite agar mudah mencari korelasi antara failure dan event dependency seperti reconnect database atau restart service.

Workflow verifikasi untuk mencegah regresi

Workflow verifikasi yang baik memisahkan tahap bootstrap, validasi environment, dan validasi aplikasi. Tujuannya agar ketika failure muncul, tim cepat tahu apakah masalahnya ada di app, test, atau environment.

Contoh struktur pipeline

stages:
  - lint
  - build
  - env-verify
  - smoke
  - integration
  - report

env-verify:
  steps:
    - start containers
    - verify network aliases
    - verify timezone/config summary
    - run dependency health checks

smoke:
  steps:
    - run migrations
    - load fixed seed data
    - run critical smoke tests

integration:
  steps:
    - run database integration tests
    - run API integration tests
    - run queue/background worker tests

report:
  steps:
    - collect app logs
    - collect dependency logs
    - collect failed test artifacts

Struktur ini tidak bergantung pada tool tertentu. Intinya adalah memisahkan validasi environment dari validasi fungsional agar sumber kegagalan lebih mudah diisolasi.

Buat matriks eksekusi lokal vs CI

Jangan mengasumsikan lokal dan CI identik hanya karena sama-sama memakai container. Buat matriks sederhana untuk mengetahui suite mana yang wajib stabil di kedua tempat.

Jenis TestLokalCICatatan
Smoke testWajibWajibHarus cepat dan jadi indikator readiness awal
Integration DBWajibWajibPakai seed tetap dan reset state
External service mockOpsionalWajibTergantung kebutuhan fitur
Heavy end-to-endTerbatasWajib terjadwal/PR tertentuJangan jadikan satu-satunya sinyal kualitas

Matriks ini membantu mencegah dua masalah umum:

  • test lokal terlalu ringan sehingga bug baru terlihat di CI,
  • test lokal terlalu berat sehingga developer jarang menjalankannya.

Tambahkan langkah pembanding environment

Salah satu teknik yang berguna adalah mencetak ringkasan environment di awal run, misalnya:

echo "TZ=$TZ"
echo "HOSTNAME=$(hostname)"
echo "APP_ENV=$APP_ENV"
echo "DB_HOST=$DB_HOST"
echo "REDIS_HOST=$REDIS_HOST"
date -u

Informasi sederhana ini sering cukup untuk menemukan mismatch yang sebelumnya tidak terlihat, seperti database host masih menunjuk ke localhost atau timezone container belum diset sesuai harapan.

Checklist migrasi dari Vagrant ke container lokal

  1. Petakan asumsi environment lama
    Catat hostname, port, shared folder, user permission, timezone, dan urutan startup yang dipakai saat masih di Vagrant.
  2. Standarkan endpoint dependency
    Pastikan aplikasi dan test memakai nama service yang konsisten, bukan localhost atau alias historis.
  3. Tentukan timezone tunggal untuk test
    Idealnya UTC di seluruh service dan runner.
  4. Pisahkan volume kerja penting
    Cache, temp file, dan artifact test sebaiknya tidak tercampur tanpa kontrol antara host dan container.
  5. Tambahkan health check readiness
    Jangan mengandalkan port terbuka atau sleep tetap.
  6. Reset state dependency sebelum suite
    Database, cache, queue, dan file temporary harus kembali ke state yang diketahui.
  7. Gunakan seed data tetap
    Fixture harus deterministik dan seminimal mungkin.
  8. Kumpulkan log environment dan test
    Minimal log aplikasi, dependency, dan ringkasan konfigurasi penting.
  9. Definisikan matriks lokal vs CI
    Putuskan suite mana yang wajib lulus di kedua environment.
  10. Tinjau retry yang sudah ada
    Hapus retry yang menutupi bug, pertahankan hanya yang benar-benar untuk kondisi transien.

Anti-pattern yang harus dihindari

1. Menambahkan sleep di mana-mana

Ini solusi semu. Test mungkin tampak stabil sebentar, tetapi build menjadi lambat dan akar masalah tetap ada.

2. Menganggap retry selalu aman

Jika test lulus setelah retry tanpa penyelidikan, Anda kehilangan sinyal penting bahwa environment atau aplikasi belum deterministik.

3. Menggunakan data acak tanpa jejak

Data acak memperburuk reproduksibilitas jika seed random tidak dicatat atau fixture berubah antar run.

4. Bergantung pada localhost di dalam container

Ini salah satu penyebab paling umum koneksi gagal saat migrasi dari VM ke container.

5. Menjalankan suite terhadap state bersama

Database, cache, atau queue bersama tanpa reset yang jelas hampir pasti menghasilkan flaky test seiring waktu.

6. Menyamakan status container hidup dengan service siap

Proses running hanyalah langkah awal. Test butuh dependency yang benar-benar ready.

Kapan bug berasal dari aplikasi, kapan dari environment?

Ini pertanyaan yang paling sering muncul setelah migrasi. Beberapa heuristik berikut membantu:

Indikasi kuat masalah environment

  • Failure terjadi di awal bootstrap atau saat koneksi dependency pertama.
  • Error berubah-ubah antara timeout, connection refused, atau DNS resolution.
  • Test yang sama lulus tanpa perubahan kode ketika dijalankan beberapa menit kemudian.
  • Log menunjukkan dependency belum ready, hostname salah, atau timezone berbeda.

Indikasi kuat bug aplikasi atau desain test

  • Assertion gagal konsisten pada skenario bisnis yang sama.
  • Race condition muncul saat concurrency meningkat atau worker berjalan paralel.
  • Test bergantung pada urutan eksekusi, state global, atau waktu nyata.
  • Perbaikan environment tidak mengubah pola failure.

Sering kali jawabannya adalah kombinasi keduanya: migrasi environment tidak menciptakan bug baru, tetapi mengekspos asumsi aplikasi yang sebelumnya tersembunyi.

Jika sebuah bug hanya muncul di container lokal, jangan langsung menandainya sebagai “masalah Docker”. Bisa jadi container justru lebih jujur dalam memperlihatkan bahwa aplikasi Anda bergantung pada timing, filesystem, atau state yang tidak eksplisit.

Penutup

Strategi terbaik untuk mengurangi flaky test saat migrasi dari Vagrant ke container lokal adalah memperlakukan environment sebagai bagian dari sistem yang perlu diverifikasi, bukan sekadar tempat menjalankan test. Fokuskan upaya pada enam area yang paling sering berubah: filesystem, startup service, waktu, jaringan, volume, dan isolasi state.

Dari sana, bangun workflow verifikasi yang sederhana tetapi disiplin: smoke test cepat, integration test yang jelas dependency-nya, seed data tetap, health check readiness, retry yang terukur, log yang cukup, dan matriks eksekusi lokal vs CI. Dengan pendekatan ini, tim bisa menikmati environment lokal yang lebih ringan tanpa menukar kecepatan dengan ketidakpastian hasil test.