.NET 8'in TimeProvider özelliği ve tek bir SynchronizationContext ayarı, aksi takdirde gerçek bekleme sürelerinde (sleep) boşta kalan yeniden deneme mantığı (retry-logic) birim testlerinden saniyeler kazanmanızı sağlayabilir. Eskiden yedi saniye süren bir test artık birkaç on milisaniyede tamamlanıyor ve bu değişiklik üretim ortamında (production) çalıştırmak için ekstra bir maliyet getirmiyor.
Gecikme neden önemlidir?
Birçok yeniden deneme yardımcısı (retry helper) üstel geri çekilme (exponential backoff) kullanır: 1 saniye bekle, sonra 2 saniye, sonra 4 saniye ve bu şekilde devam eder. Üretim ortamında bu, servisleri başarısız olan bir uç noktayı (endpoint) aşırı yüklemekten korur. Bir test paketinde ise aynı kod gerçek bir saat üzerinde Task.Delay çağırır, bu nedenle test iş parçacığı (thread) gerçekten uyur. Bunu onlarca testle çarptığınızda, sürekli entegrasyon (CI) hattı hiçbir işlevsel kazanç sağlamadan dakikalarca uzar. "Uyku vergisi" (sleep tax) tamamen boşa harcanan zamandır.
TimeProvider nasıl çalışır?
.NET 8, sistem saati üzerinde bir soyutlama olan TimeProvider'ı tanıttı. Mevcut zamana ihtiyaç duyan veya gecikme gerektiren bir sınıf, DateTime.UtcNow veya Task.Delay metodlarını doğrudan çağırmak yerine bir TimeProvider örneği (instance) kabul edebilir. Üretim ortamında, gerçek saate yönlendirme yapan TimeProvider.System nesnesini geçersiniz. Bir testte ise bir FakeTimeProvider sağlarsınız. Sahte sağlayıcı, Advance(TimeSpan) ile zamanı manuel olarak ilerletmenize olanak tanır. Dahili olarak Task.Delay sağlayıcıyı okur, bu nedenle sahte saati ileri almak, bekleyen tüm gecikmeleri anında karşılar.
Fikir basit: Gerçek saati kontrol edilebilir bir saatle değiştirin, ardından test edilen kodun devam edeceği noktaya ileriye atlayın.
xUnit SynchronizationContext tuzağı
Bu kavram bir konsol uygulamasında çalışır, ancak xUnit'te bir kilitlenmeye (deadlock) neden oldu. Yeniden deneme yardımcısı bir gecikmeyi beklerken (await), test Advance() metodunu çağırdı. xUnit, devam eden işlemleri (continuations) yakalayan ve bunları test iş parçacığında çalıştıran kendi SynchronizationContext yapısını kurar. Advance() saati ileri aldığında, devam eden işlem test iş parçacığı yerine iş parçacığı havuzuna (thread pool) kuyruğa alındı. Test iş parçacığı zamanı ilerletmeye devam ederek simüle edilen saati, bir sonraki yeniden denemenin gerçekleşmesi gereken anın ötesine taşıdı. Yeni zamanlayıcı, saati tekrar ilerletecek hiçbir iş parçacığı kalmadığı için asla ulaşılamayacak gelecekteki bir noktaya ayarlandı. Test sonsuza kadar asılı kaldı.
Tek satırlık çözüm
Testin başına tek bir satır eklemek beklenen davranışı geri kazandırır:
SynchronizationContext.SetSynchronizationContext(null);
Özel bağlamı (custom context) temizlemek, await devam eden işlemlerinin, sahte zamanlayıcının geri çağırmalarının (callbacks) yürütülebileceği iş parçacığı havuzunda çalışmaya zorlar. Bu değişiklikle aynı test süresi yedi saniyeden yaklaşık 36 milisaniyeye düşer.
Daha geniş faydalar
Boşta beklemeleri ortadan kaldırmanın ötesinde, sahte bir saat kullanmak zaman dilimi hassasiyeti olan mantığı test etmeyi kolaylaştırır. Gerçek saatin gece yarısını vurmasını beklemeden, günlük bir kotanın doğru yerel gece yarısında sıfırlandığını doğrulayabilirsiniz. Aynı yaklaşım, mevcut zamana göre dallanan (branching) tüm kodlar için geçerlidir: sağlayıcıyı enjekte edin, gizli Thread.Sleep veya Task.Delay çağrılarından kaçının ve deterministik, hızlı testler elde edin.
Nelere dikkat edilmeli?
- Her gecikme, enjekte edilen
TimeProviderüzerinden yönlendirilmelidir. Rastgele birThread.Sleepveya doğrudan birTask.Delayyine gerçek saati çağıracak ve gecikmeyi tekrar getirecektir. - Bağımlı olduğunuz bir kütüphane, bir sağlayıcı sunmadan dahili olarak
Task.Delayçağırıyorsa, daha müdahaleci bir ara katman (shim) olmadan zamanlamasını kontrol edemezsiniz. Bu gibi durumlarda fayda sınırlı olabilir. - Gizli beklemeleri tespit etmek için test klasöründe
Task.DelayveThread.Sleepifadelerini aratın (grep). Bu çağrıları sağlayıcıyı kullanacak şekilde yeniden düzenlemek (refactoring), sahte saatin etkili kalmasını sağlamanın tek yoludur.
Özet
Sistem saatini TimeProvider ile değiştirmek ve xUnit'in SynchronizationContext yapısını devre dışı bırakmak, hantal yeniden deneme testlerini neredeyse anlık kontroller haline getirir; böylece CI kaynaklarını serbest bırakır ve zamana bağlı kodun doğrulanmasını kolaylaştırır. Harcanan çaba sadece birkaç satır koddur; karşılığı ise test çalıştırma başına tasarruf edilen saniyelerle ölçülür.
