.NET 8’s TimeProvider dan satu pelarasan SynchronizationContext boleh menjimatkan beberapa saat bagi ujian unit logik cubaan semula (retry-logic) yang sebelum ini terhenti lama kerana menunggu masa tidur (sleep) yang sebenar. Ujian yang dahulunya mengambil masa tujuh saat kini selesai dalam beberapa milisaat sahaja, dan perubahan ini tidak melibatkan kos tambahan untuk dijalankan dalam pengeluaran (production).
Mengapa kelewatan itu penting
Banyak pembantu cubaan semula (retry helpers) menggunakan exponential backoff: tunggu 1 saat, kemudian 2 saat, kemudian 4 saat, dan seterusnya. Dalam pengeluaran, ini melindungi perkhidmatan daripada membebankan titik akhir (endpoint) yang gagal. Dalam suite ujian, kod yang sama memanggil Task.Delay pada jam sebenar, jadi bebenang (thread) ujian sebenarnya tidur. Darabkan perkara itu dengan berpuluh-puluh ujian dan saluran paip integrasi berterusan (CI) akan meningkat selama beberapa minit tanpa sebarang faedah fungsian. "Cukai tidur" ini hanyalah pembaziran masa semata-mata.
Cara TimeProvider berfungsi
.NET 8 memperkenalkan TimeProvider, sebuah abstraksi ke atas jam sistem. Kelas yang memerlukan masa semasa atau perlu menangguhkan sesuatu boleh menerima instans TimeProvider dan bukannya memanggil DateTime.UtcNow atau Task.Delay secara terus. Dalam pengeluaran, anda menghantar TimeProvider.System, yang memanjangkan arahan ke jam sebenar. Dalam ujian, anda membekalkan FakeTimeProvider. Versi palsu ini membolehkan anda memajukan masa secara manual dengan Advance(TimeSpan). Secara dalaman, Task.Delay membaca pembekal tersebut, jadi memajukan jam palsu tersebut akan memenuhi sebarang penangguhan yang tertunda dengan serta-merta.
Idea ini mudah: gantikan jam sebenar dengan jam yang boleh dikawal, kemudian lompat ke hadapan ke titik di mana kod yang diuji sepatutnya disambung semula.
Perangkap SynchronizationContext xUnit
Konsep ini berfungsi dalam aplikasi konsol, tetapi dalam xUnit ia menyebabkan kebuntuan (deadlock). Ujian memanggil Advance() semasa pembantu cubaan semula sedang menunggu penangguhan. xUnit memasang SynchronizationContext sendiri yang menangkap kesinambungan (continuations) dan menjalankannya pada bebenang ujian. Apabila Advance() memajukan jam, kesinambungan tersebut dimasukkan ke dalam barisan (queue) ke dalam kolam bebenang (thread pool) dan bukannya bebenang ujian. Bebenang ujian terus memajukan masa, menolak jam simulasi melepasi saat di mana cubaan semula seterusnya sepatutnya dicetuskan. Pemasa baharu ditetapkan untuk titik masa hadapan yang tidak akan pernah dicapai kerana tiada bebenang yang tinggal untuk menggerakkan jam itu lagi. Ujian tersebut tergantung tanpa henti.
Penyelesaian satu baris
Menambah satu baris sahaja pada permulaan ujian akan memulihkan tingkah laku yang diharapkan:
SynchronizationContext.SetSynchronizationContext(null);
Membersihkan konteks tersuai memaksa kesinambungan await berjalan pada kolam bebenang, di mana panggilan balik (callbacks) pemasa palsu boleh dilaksanakan. Dengan perubahan itu, ujian yang sama berkurang daripada tujuh saat kepada kira-kira 36 milisaat.
Faedah yang lebih luas
Selain menghapuskan masa tidur yang tidak aktif, jam palsu memudahkan ujian logik yang sensitif terhadap zon masa. Anda boleh mengesahkan bahawa kuota harian ditetapkan semula pada tengah malam tempatan yang betul tanpa menunggu jam sebenar menunjukkan pukul dua belas. Pendekatan yang sama berfungsi untuk sebarang kod yang mempunyai cawangan (branching) berdasarkan masa semasa: suntik (inject) pembekal tersebut, elakkan panggilan tersembunyi kepada Thread.Sleep atau Task.Delay, dan anda akan mendapat ujian yang pantas serta deterministik.
Perkara yang perlu diperhatikan
- Setiap penangguhan mesti disalurkan melalui
TimeProvideryang disuntik.Thread.Sleepyang tersasar atauTask.Delaysecara terus akan tetap memanggil jam sebenar dan memperkenalkan semula kependaman (latency). - Jika perpustakaan (library) yang anda gunakan memanggil
Task.Delaysecara dalaman tanpa mendedahkan pembekal, anda tidak dapat mengawal pemasaannya tanpa shim yang lebih invasif. Dalam kes sedemikian, faedahnya mungkin terhad. - Gunakan
greppada folder ujian untuk mencariTask.DelaydanThread.Sleepbagi mengesan penangguhan tersembunyi. Melakukan penambahbaikan (refactoring) pada panggilan tersebut untuk menggunakan pembekal adalah satu-satunya cara untuk memastikan jam palsu kekal berkesan.
Kesimpulan
Menggantikan jam sistem dengan TimeProvider dan menyahaktifkan SynchronizationContext xUnit menukarkan ujian cubaan semula yang lembap kepada semakan yang hampir serta-merta, membebaskan sumber CI dan menjadikan kod yang bergantung pada masa lebih mudah untuk disahkan. Usahanya hanyalah beberapa baris kod; hasilnya diukur dalam saat yang dijimatkan bagi setiap larian ujian.
