TimeProvider pada .NET 8 dan satu penyesuaian SynchronizationContext dapat memangkas waktu beberapa detik pada unit test logika retry yang biasanya tertahan karena proses sleep yang nyata. Sebuah tes yang dulunya memakan waktu tujuh detik kini selesai dalam beberapa puluh milidetik, dan perubahan ini tidak memberikan biaya tambahan saat dijalankan di produksi.

Mengapa penundaan itu penting

Banyak helper retry menggunakan exponential backoff: tunggu 1 detik, lalu 2 detik, lalu 4 detik, dan seterusnya. Di produksi, hal ini melindungi layanan agar tidak membombardir endpoint yang gagal. Dalam rangkaian pengujian (test suite), kode yang sama memanggil Task.Delay pada jam sistem yang nyata, sehingga thread pengujian benar-benar tertidur (sleep). Kalikan hal tersebut dengan puluhan tes, dan pipeline continuous-integration (CI) akan membengkak hingga hitungan menit tanpa keuntungan fungsional apa pun. "Pajak tidur" (sleep tax) ini adalah pemborosan waktu murni.

Cara kerja TimeProvider

.NET 8 memperkenalkan TimeProvider, sebuah abstraksi di atas jam sistem. Kelas yang membutuhkan waktu saat ini atau perlu melakukan penundaan dapat menerima instans TimeProvider alih-alih memanggil DateTime.UtcNow atau Task.Delay secara langsung. Di produksi, Anda meneruskan TimeProvider.System, yang meneruskan perintah ke jam nyata. Dalam pengujian, Anda menyediakan FakeTimeProvider. Fake provider ini memungkinkan Anda memajukan waktu secara manual dengan Advance(TimeSpan). Secara internal, Task.Delay membaca provider tersebut, sehingga memajukan jam palsu secara instan akan memenuhi semua penundaan yang tertunda.

Idenya sederhana: ganti jam nyata dengan jam yang dapat dikendalikan, lalu lompat ke titik di mana kode yang sedang diuji seharusnya dilanjutkan.

Jebakan SynchronizationContext pada xUnit

Konsep ini berhasil di aplikasi konsol, tetapi di xUnit hal ini menyebabkan deadlock. Tes memanggil Advance() saat helper retry sedang menunggu (awaiting) penundaan. xUnit memasang SynchronizationContext miliknya sendiri yang menangkap continuation dan menjalankannya pada thread pengujian. Ketika Advance() memajukan jam, continuation tersebut dimasukkan ke dalam antrean thread pool alih-alih ke thread pengujian. Thread pengujian terus memajukan waktu, mendorong jam simulasi melewati momen saat retry berikutnya seharusnya dijalankan. Timer baru diatur untuk titik di masa depan yang tidak akan pernah tercapai karena tidak ada thread yang tersisa untuk memajukan jam lagi. Tes tersebut pun macet (hang) tanpa henti.

Perbaikan satu baris

Menambahkan satu baris di awal tes akan mengembalikan perilaku yang diharapkan:

SynchronizationContext.SetSynchronizationContext(null);

Menghapus konteks kustom memaksa continuation await untuk berjalan di thread pool, di mana callback dari timer palsu dapat dieksekusi. Dengan perubahan tersebut, tes yang sama turun dari tujuh detik menjadi sekitar 36 milidetik.

Manfaat yang lebih luas

Selain menghilangkan waktu tunggu yang menganggur, jam palsu memudahkan pengujian logika yang sensitif terhadap zona waktu. Anda dapat memverifikasi bahwa kuota harian diatur ulang pada tengah malam waktu setempat yang tepat tanpa menunggu jam nyata menunjukkan pukul dua belas. Pendekatan yang sama berlaku untuk kode apa pun yang bercabang berdasarkan waktu saat ini: suntikkan (inject) provider-nya, hindari panggilan tersembunyi ke Thread.Sleep atau Task.Delay, dan Anda akan mendapatkan pengujian yang deterministik dan cepat.

Hal yang perlu diperhatikan

  • Setiap penundaan harus diarahkan melalui TimeProvider yang disuntikkan. Thread.Sleep yang terselip atau Task.Delay secara langsung akan tetap memanggil jam nyata dan memunculkan kembali latensi.
  • Jika pustaka (library) yang Anda gunakan secara internal memanggil Task.Delay tanpa mengekspos provider, Anda tidak dapat mengontrol waktunya tanpa shim yang lebih invasif. Dalam kasus seperti ini, manfaatnya mungkin terbatas.
  • Gunakan grep pada folder tes untuk mencari Task.Delay dan Thread.Sleep guna menemukan penundaan tersembunyi. Melakukan refaktorisasi panggilan tersebut agar menggunakan provider adalah satu-satunya cara agar jam palsu tetap efektif.

Kesimpulan

Mengganti jam sistem dengan TimeProvider dan menonaktifkan SynchronizationContext milik xUnit mengubah tes retry yang lamban menjadi pemeriksaan yang hampir instan, membebaskan sumber daya CI, dan membuat kode yang bergantung pada waktu lebih mudah diverifikasi. Upayanya hanya beberapa baris kode; hasilnya terukur dalam detik yang dihemat per sesi pengujian.