Tarihçe: Zamana bağlı mantığın test edilmesi geleneksel olarak System.currentTimeMillis() çağrıları veya Thread.sleep() ifadelerine dayanıyordu ve bu, gece yarısına yakın çalıştırıldığında sıklıkla arızalanan, kırılgan ve yavaş testler oluşturuyordu. Erken otomasyon çerçeveleri, Docker konteynerleri içindeki işletim sistemi saatlerini manipüle etmeyi denediler, ancak bu, paylaşılan CI/CD altyapısında zincirleme arızalara neden oldu. Modern yaklaşımlar, zamanın bir bağımlılık olarak değerlendirilmesi gerektiğini, veritabanları veya HTTP hizmetleri gibi, soyutlama katmanları aracılığıyla belirleyici kontrol sağladığını kabul ediyor.
Sorun: Dağıtılmış mikroservisler, yerel zamanların atlandığı veya tekrar edildiği DST geçişlerini, UTC'ye ek süre ekleyen artı saniyeleri ve var olmayan saatleri referans alabilen cron ifadelerini ele almak zorundadır. Uygun izolasyon olmadan, "ay sonu" işlemleri için testler zamana bağlı sınırlar yakınında başarısız hale gelebilir. Ayrıca, 40'tan fazla global zaman dilimi boyunca davranışı doğrulamak, gerçek zaman ilerleyişini kullanarak yıllar alacak binlerce test permütasyonu gerektirir.
Çözüm: Java'da mevcut olan Clock arayüzünü kullanarak bir TimeProvider soyutlaması uygulamak, donmuş, kaydırılmış veya hızlandırılmış zaman kaynaklarının enjekte edilmesine olanak tanır. Bunu, gerçek veritabanı örnekleri çalıştıran TestContainers ile birleştirin, ancak uygulama saatini soyutlama aracılığıyla kontrol edin, konteyner işletim sistemi saatleri yerine. Tutarlı davranışı sağlamak için zaman dilimi geçiş veri setleri üzerinde yinelemek amacıyla JUnit parametreli testlerini kullanın.
public interface TimeProvider { Instant now(); ZonedDateTime nowInZone(ZoneId zone); } public class MutableClock implements TimeProvider { private Instant frozenInstant; public void setTime(Instant instant) { this.frozenInstant = instant; } @Override public ZonedDateTime nowInZone(ZoneId zone) { return frozenInstant.atZone(zone); } } public class BillingScheduler { private final TimeProvider clock; public BillingScheduler(TimeProvider clock) { this.clock = clock; } public boolean isEndOfBillingCycle(LocalDate date, ZoneId zone) { ZonedDateTime now = clock.nowInZone(zone); return now.toLocalDate().equals(date) && now.getHour() == 0; } } @Test public void testDSTSpringForward() { MutableClock clock = new MutableClock(); clock.setTime(Instant.parse("2024-03-10T07:30:00Z")); BillingScheduler scheduler = new BillingScheduler(clock); // Doğrulama mantığı burada }
Ayırt edici örnek: Küresel bir fintech platformu, @Scheduled(cron = "0 0 2 * * ?") ile yapılandırılmış günlük aşım ücretlerini hesaplıyordu. Mart 2023'te ABD'deki DST geçişinde, Doğu zaman dilimindeki müşteriler, görev hem "eski" 2:00 AM (EST) hem de "yeni" 2:00 AM (EDT) teslim edildiği için iki kez ücretlendirildi. QA ekibi, bu tekrarın önlenmesi ve düzeltmenin 12 diğer uluslararası pazarda farklı DST kurallarıyla çalıştığından emin olması gerekiyordu.
Problem açıklaması: Mevcut test paketi, gerçek zaman ilerleyişini beklemek için Awaitility'ye dayanıyordu, bu yüzden DST testleri belirli tarihlerde sadece 2:00 AM'da manuel yürütme olmadan mümkün olmuyordu. Ekip, quartz zamanlayıcısının "eksik saat" kuralına uyduğunu ve veritabanı zaman damgalarının UTC'de doğru bir şekilde yerel iş tarihleri ile eşleştiğini doğrulamak zorundaydı.
Düşünülen farklı çözümler:
Çözüm 1: Ayrıcalıklı Konteyner Saati Manipülasyonu
Ekip, sistem tarihini date komutunu kullanarak değiştirmek için --privileged bayraklarıyla Docker konteynerleri çalıştırmayı düşündü. Bu, gerçek JVM zaman dilimi veritabanını ve işletim sistemi düzeyindeki cron davranışını test edecek.
Artıları: Üretim altyapısıyla maksimum bağlılık; gerçek libc zaman dilimi işleyişini doğrular.
Eksileri: Test paralelleşmesini bozar, çünkü ana saat değişiklikleri tüm konteynerleri etkiler; Kubernetes güvenlik bağlamı ihlalleri gerektirir; saat ayarlamaları sırasında yarış koşullarından dolayı kırılgan testler oluşturur.
Çözüm 2: Gözlem Temelli Programlama Kesintisi
Uygulama kodunu değiştirmeden test kontrollü bir kaynağa yönlendirmek için AspectJ ile java.time.Instant.now() çağrılarını kesmek.
Artıları: Miras monolitler için sıfır yeniden yapılandırma gerektirir; standart zaman API'lerini kullanan üçüncü taraf kütüphanelerle çalışır.
Eksileri: Karmaşık bayt kodu dokuma yapılandırması; daha yeni JDK'lerde Java modül sistemi (JPMS) ile bozulur; Jackson serileştiricilerinde özel zaman ayrıştırma mantığını test etmez.
Çözüm 3: Bağımlılık Enjeksiyonu ile Mimarinin Yeniden Yapılandırılması
Bütün zaman ile ilgili bileşenlerin, üretimde sistem saatini sağlamak ve JUnit testlerinde test parçaları kullanmak üzere bir Clock arayüzünü inşaatçı enjeksiyonu ile kabul edecek şekilde yeniden yapılandırılması.
Artıları: Belirleyici, anlık test yürütmesi; aynı anda birden fazla zaman diliminin paralel test edilmesini destekler; artı yıl olmayan yıllarda Şubatta 29'u test etmeyi mümkün kılar.
Eksileri: Statik LocalDateTime.now() çağrılarını yeniden yapılandırmak için önceden geliştirme çabası gerektirir; takımın, geliştiricilerin soyutlamayı atlamasını önlemek için eğitim alması gerekir.
Seçilen çözüm ve neden: Çözüm 3'ü seçtik çünkü bu, saatler yerine milisaniyeler içinde belirleyici geri bildirim sağladı. Ekip, Java'nın java.time.Clock kullanarak bir TimeContext sınıfı uyguladı ve iki sprintte 150'den fazla servis sınıfını yeniden yapılandırdı. Bunun yanı sıra, üretim düzeyindeki sorunları yakalamak için izole bir AWS hesabında Çözüm 1 ile bir günlük "zamansal kaos" testi gerçekleştirdik.
Sonuç: Çerçeve, üretime geçmeden önce Brezilya zaman dilimiyle ilgili yedi kritik hatayı tanımladı. Zamanlama modülünün test yürütme süresi 4 saatten 45 saniyeye düştü. Çözüm, daha önce belirli astronomik olayları beklemeyi gerektiren "artı saniye" senaryolarını test etmeyi sağladı.
Soru 1: 1:30 AM iki kez meydana geldiğinde, bir zamanlanmış işin tam olarak bir kez yürütüldüğünü nasıl doğrularsınız?
Cevap: Adaylar genellikle yerel saat dizesini kontrol etmeyi öneriyor, bu da her iki olay için de 1:30 AM gösterilecektir. Doğru yaklaşım, yerel zamanla birlikte ZoneOffset bileşeninin doğrulanmasını gerektirir. Java'da, ofset içeren ZonedDateTime kullanın. Test, saati ilk olaya (EDT) dondurmalıdır, işi tetiklemeli ve veritabanı durumunun değiştiğini doğrulamalıdır, ardından tam olarak bir saat ileri gitmeli ve ikinci olaya (EST) geçerek işin görevi zaten tamamlanmış olarak tanıdığını doğrulamalıdır. Bu, Zamanda bilgilerini içeren ZonedDateTime parametrelerini destekleyecek şekilde TimeProvider'ın sağlanmasını gerektirir; böylece idempotentlik kontrolleri, UTC zaman çizelgesinde iki anı ayırt eder.
Soru 2: Zaman dilimlerinde test ederken, veritabanı TIMESTAMP WITHOUT TIME ZONE sütunlarının DST ile ilgili hayalet hatalar yaratmasını nasıl önlersiniz?
Cevap: Çoğu aday yalnızca uygulama koduna odaklanıyor, ancak kalıcılık katmanı davranışını atlıyor. Yerel iş tarihlerinin PostgreSQL veya MySQL'de TIMESTAMP WITHOUT TIME ZONE olarak saklanması, ofset bağlamını kaybettirir. DST geçişlerinde, iki kez saklanan aynı yerel zaman aslında UTC'de iki farklı anı temsil eder. Test stratejisi, "geri dönüş" saatindeki kayıtları iki kez saymadığından emin olmak için BETWEEN ifadeleri kullanan sorguları doğrulamalıdır. TestContainers ile gerçek veritabanı örnekleri kullanarak, her iki 1:30 AM olayında kayıtları yerleştirin ve saat kaynağını kontrol etmek için Clock soyutlamasını kullanarak anları kontrol edin, ardından günlük toplama sorgularının doğru toplamları döndürdüğünü doğrulayın.
Soru 3: Farklı uzunluklara sahip aylar söz konusu olduğunda "L" (ayın son günü) gibi uç durumlar için cron ifadesi ayrıştırmasını nasıl test edersiniz, ay sonunu beklemeden?
Cevap: Adaylar genellikle Quartz gibi cron kütüphanelerinin sonraki yürütme zamanlarını mevcut zamana göre hesapladığını gözden kaçırıyor. Artı yıllarındaki 29 Şubat davranışını test etmek için, yalnızca yürütme zamanında saati taklit etmek yeterli değildir. Zamanlayıcıdan hesaplanacak "sonraki" yürütmeyi görmek için değerlendirme zamanında saati taklit etmelisiniz. Çözüm, mevcut saati 28 Şubat 11:59 PM'e ayarlamak, zamanlayıcının sonraki yürütme hesaplamasını sorgulamak, bunun Şubat 29 veya Mart 1 döndürdüğünü doğrulamak ve sonra gerçek yürütmeyi test etmek için saati ilerletmeyi içerir. Bu, zamanlayıcının tetikleme hesaplama API'sini testlerde açmak veya taklit saat ile Awaitility kullanmak gerektirir.