Ders 05 / 19
Test Kararlılığı
Aynı kodda bazen geçen bazen kalan test kırılgandır. Ders sabit tur bekleyen bir testin hazır olma turuyla bütçeyi kaç koşumda aştığını, koşula bağlı yoklamanın bunu nasıl kapattığını ölçer.
Yayın Güncelleme
Bu derste
- Kırılgan testi aynı kodda koşumdan koşuma değişen sonuç olarak tanımlar
- Sabit tur bekleyen testin hazır olma turuyla bekleme bütçesini kaç koşumda aştığını ölçer
- Beklemeyi uzatmanın ve koşula bağlı yoklamanın bütçeye etkisini karşılaştırır
- Eşitleme için boşta kaynağı kavramını platform parçasında tanır
İçindekiler
Önceki derste test ekranı bulabiliyordu. Ama ekran hazır olmadan bulmaya kalkarsa, aynı test bir koşumda geçer, ötekinde kalır. Bu derste ölçülen iş birimi bir bekleme turudur: testin ekranın hazır olmasını beklerken harcadığı tur. Kursun sorusu değişmez: bu ölçüm bütçesini kaç örnekte aşıyor? Yanıtlar yine içinde, azında, çoğunda ve hepsinde.
Çekirdek önceki derslerle aynıdır.
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)
class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
val total: Int = samples.size
val over: Int = samples.count { it > budget.limit }
val verdict: String = when {
over == 0 -> "icinde"
over == total -> "hepsinde"
over * 2 < total -> "azinda"
else -> "cogunda"
}
fun line() {
println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
}
}
fun countOverruns(overruns: List<Overrun>) {
for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
println("$name: ${overruns.count { it.verdict == name }}")
}
}
Kırılgan Testin Anatomisi
Kırılgan test (flaky test), kod değişmediği hâlde bazen geçen bazen kalan testtir. Nedeni çoğunlukla testte değil, testin sıraya koyduğu şeylerdedir: ekranın çizilmesi, ağ yanıtı, bir eş yordamın bitişi. Test bunların ne zaman olacağını bilmez ve bir tahmin yapar: “iki saniye sonra hazırdır”. Tahmin çoğu koşumda tutar, bir koşumda tutmaz.
Bu sorun üç ayrı soruyla karışır. M30/K05’te bir sonucun yinelemeyle ya da sırayla değişip değişmediğini sormuştuk. M13/K04’te bir sonucu neyin belirlediğini sorduk. Burada ikisi de yok: kırılganlığı bir aşım kategorisi olarak adlandırmıyoruz ve kaynağını da aramıyoruz. Ölçüm yalnızca aşımın kaç koşuma yayıldığını sayar. Kırılganlık bir kategori değildir; aşımı üreten düzenektir, yani bekleme biçimidir.
Platformda bu soruna karşı kullanılan araç, eşitleme (synchronization) sağlayan boşta kaynağıdır (idling resource). Kaynak, uygulamanın meşgul olup olmadığını teste bildirir ve test, kaynak boşa çıkana kadar işlemini erteler.
// Platform kodu: BusyCounterResource
class BusyCounterResource : IdlingResource {
private val busy = AtomicInteger(0)
private var callback: IdlingResource.ResourceCallback? = null
override fun getName(): String = "busy-counter"
override fun isIdleNow(): Boolean = busy.get() == 0
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
fun begin() { busy.incrementAndGet() }
fun end() {
if (busy.decrementAndGet() == 0) callback?.onTransitionToIdle()
}
}
begin iş başlarken, end iş biterken çağrılır. isIdleNow sıfırsa test ilerler. Test süre tahmin etmez; uygulamanın kendisine sorar. Model bu araçları koşturmaz; bunun yerine ekranın hazır olduğu turu bir diziden okur ve testin beklemeyi nasıl yaptığına göre harcanan turu sayar.
TA1. Modelde hazır olma turu sabit bir dizidir; gerçek bir koşumda bu değer ağın, işlemcinin ve sıranın o anki durumuna bağlıdır. Dizi, sekiz koşumun hazır olma turunu temsil eder ve rastgele üretilmez.
Sabit Tur Bekleyen Test
Yardımcı dosya hazır olma turlarını ve iki bekleme biçimini tanımlar.
// Stability.kt — hazir olma turlari ve iki bekleme bicimi val readyTable: List<Int> = listOf(3, 4, 3, 8, 4, 3, 9, 4) fun pollRounds(run: Int): Int = readyTable[run] fun fixedWaitRounds(run: Int, wait: Int): Int = wait fun passesAfterFixedWait(run: Int, wait: Int): Boolean = pollRounds(run) <= wait
pollRounds(run) o koşumda ekranın kaçıncı turda hazır olduğunu verir. fixedWaitRounds testin sabit tahminini: ne olursa olsun wait tur bekler. passesAfterFixedWait testin geçip geçmediğini: ekran wait turdan önce hazırsa geçer.
Test, ekranın beş turda hazır olacağını varsayıyor. Sekiz koşumun hazır olma turları ölçülür; bütçe, testin beklediği 5 turdur.
// Main.kt
fun main() {
val runs = (0 until 8).toList()
val ready = runs.map { pollRounds(it) }
println("hazir olma turlari: $ready")
val failed = runs.count { !passesAfterFixedWait(it, 5) }
println("bekleme 5 tur, kalan kosum: $failed")
val overrun = Overrun("sabit-bekleme", Budget("tur", 5), ready)
overrun.line()
countOverruns(listOf(overrun))
}
hazir olma turlari: [3, 4, 3, 8, 4, 3, 9, 4] bekleme 5 tur, kalan kosum: 2 sabit-bekleme: butce=tur<=5 | asan=2/8 -> azinda icinde: 0 azinda: 1 cogunda: 0 hepsinde: 0
Sekiz koşumun ikisinde ekran beş turdan sonra hazır oldu: dördüncü koşum 8, yedinci koşum 9 turda. Test bu iki koşumda kalır, öteki altısında geçer. Ölçüm azındadır. Sekizde altı geçen bir test güvenilir sayılmaz: kalan iki koşum ekip için gürültüdür. Kural aşımın sayısını bilir, ama o gürültünün bir regresyon mu yoksa hazır olma turu mu olduğunu ayırt etmez.
Beklemeyi Uzatmak
İlk akla gelen çare beklemeyi uzatmaktır. Test dokuz tur beklerse ekran her koşumda hazırdır ve test hiç kalmaz. Ama bunun bedeli vardır: ekran üç turda hazır olduğunda da test dokuz tur bekler. Bütçeyi bu kez bir koşumun harcayabileceği tur sayısına, yani 5’e koyalım.
// Main.kt — beklemeyi dokuza cikarmak
fun main() {
val runs = (0 until 8).toList()
val failed = runs.count { !passesAfterFixedWait(it, 9) }
println("bekleme 9 tur, kalan kosum: $failed")
val spent = runs.map { fixedWaitRounds(it, 9) }
println("harcanan turlar: $spent")
val overrun = Overrun("uzun-bekleme", Budget("tur", 5), spent)
overrun.line()
countOverruns(listOf(overrun))
}
bekleme 9 tur, kalan kosum: 0 harcanan turlar: [9, 9, 9, 9, 9, 9, 9, 9] uzun-bekleme: butce=tur<=5 | asan=8/8 -> hepsinde icinde: 0 azinda: 0 cogunda: 0 hepsinde: 1
Test kararlı oldu ama her koşumda bütçeyi aştı: hepsinde. Kırılganlık gitti, yerine sürekli bir maliyet geldi. Sabit bekleme iki hata türünden birini seçtirir: ya bazı koşumlarda kalan test ya da her koşumda yavaş test.
TA2. Bekleme turu modelde ortak bir birimdir. Gerçek bir testte bir tur, tek bir sabit süre değil, değişken uzunlukta bir bekleme adımıdır; model onu bir iş birimine indirger.
Kırılganlığın Kaynakları
Bir testin kırılgan olması için kodun yanlış olması gerekmez; test ile uygulama arasındaki zamanlama ilişkisinin yanlış kurulması yeter. Zamanlama kaynaklı kırılganlığın üç tipik sahnesi vardır.
Birincisi arayüz iş parçacığıdır. Uygulama arayüzü kendi iş parçacığında çizer; test ise başka bir iş parçacığında koşar ve bir düğmeye dokunduktan hemen sonra sonucu okumaya kalkabilir. Çizim henüz bitmemişse okuma eski durumu görür.
İkincisi arka plan işidir. Ağdan gelen bir yanıt, bir veri tabanı yazması ya da bir eş yordamın bitişi testin kontrolünde değildir. Test bunun ne zaman biteceğini bilmediği için bir tahmin yürütür ve tahmin tutmayabilir.
Üçüncüsü animasyon ve geçiştir. Bir ekran açılırken düğümler yerini bulana kadar geçici durumlarda kalır. Test geçişin ortasında düğüme dokunursa, düğüm henüz tıklanabilir olmayabilir.
Bu üç sahnenin ortak noktası, testin bir olayın bittiğini varsaymasıdır. Doğru çare olayın bittiğini sormaktır, varsaymak değildir. Modelde ekranın hazır olduğu tur bu olayın karşılığıdır ve test onu ya tahmin eder ya da sorar.
Eşitleme Seçenekleri
Testle uygulamayı eşitlemenin dört yolu vardır ve her biri farklı bir bedel taşır.
Sabit bekleme en basitidir ve bu derste ilk iki ölçümde kullanıldı. Test belirli bir süre ya da tur sayısı kadar durur. Sorunu şudur: bekleme kısaysa test kırılır, uzunsa her koşumda boşuna bekler.
Yoklama (polling), testin kısa aralıklarla bir koşulu denetlemesidir. Koşul sağlanınca test ilerler; belirli bir üst sınıra varılırsa test başarısız olur. Son ölçümde kullandığımız koşula bağlı bekleme budur. Yoklamanın bedeli, her denetimin kendi işini yapmasıdır.
Boşta kaynağı, uygulamanın kendisinin “meşgulüm” ya da “boştayım” demesidir. Yoklamadan farkı, koşulu testin değil uygulamanın bilmesidir. Uygulama bir iş başlatırken sayacı artırır, bitirirken azaltır; test sayaç sıfırlanana kadar bekler. Kaynak platform parçasında gösterilen sınıftır.
Eş yordamlar için test zamanlayıcısı, sanal bir saat kullanır ve beklemeleri gerçek zaman geçmeden ilerletir. Bu sanal saat, testin zamanı kontrol edebildiği için en kararlı seçenektir, ama yalnız eş yordam ve zamanlayıcıyla sınırlı işlerde kullanılabilir; ağ ya da çizim bu saati bilmez. Bu derste kitaplığın test paketi kurulu olmadığı için sanal saat ölçülmez, yalnız adı geçer.
Boşta Kaynağının Satır Satır Okunuşu
BusyCounterResource, test çerçevesinin tanıdığı kaynak arayüzünü uygular. busy bir AtomicIntegerdır: sayaç birden çok iş parçacığından güvenle artırılıp azaltılabilir. getName() kaynağın hata iletilerinde görünecek adını verir. isIdleNow() test çerçevesinin sorduğu sorudur: sayaç sıfırsa uygulama boştadır.
registerIdleTransitionCallback çerçevenin bir geri çağırım kaydetmesini sağlar; kaynak boşa çıktığında çerçeveye haber vermek için saklanır. begin() uygulama bir işe başlarken çağrılır ve sayacı artırır. end() iş bittiğinde sayacı azaltır; sayaç sıfıra inerse onTransitionToIdle() çağrılır ve test çerçevesi beklemeyi bırakır.
En sık yapılan hata begin çağrısını yapıp end çağrısını bir hata yolunda unutmaktır. O zaman sayaç hiç sıfırlanmaz ve test sonsuza kadar bekler. Çare, end çağrısını bir finally bloğuna koymaktır.
Bütçenin Nasıl Konduğu
Bekleme bütçesi iki farklı soruyu yanıtlar ve ikisini karıştırmak yaygın bir hatadır. Birinci soru “test en çok kaç tur beklemeye razı” sorusudur. İkinci soru “uygulama en çok kaç turda hazır oluyor” sorusudur. Sabit beklemede bu iki soru tek sayıyla yanıtlanır: test, uygulamanın en çok kaç turda hazır olacağını tahmin eder ve o kadar bekler. Tahmin gözlenen değerlere dayanıyorsa yeterli görünür, ama gözlenen değerler yeni koşumları kapsamaz.
Koşula bağlı beklemede ikisi ayrılır. Test hazır olunca ilerler; üst sınır yalnız uygulamanın hiç hazır olmadığı durumda devreye girer. Bu derste üst sınırı gözlenen en kötü tur olarak koyduk: 9. Sekiz koşum kısa bir süreyi temsil eder, bu yüzden gerçek bir ekip üst sınırı gözlenen en kötü turun üstünde, bir güvenlik payıyla koyar. Bu pay bir ürün kararıdır.
Kararlılığı Ölçmek
Kırılgan bir testi onarmadan önce ne kadar kırılgan olduğunu bilmek gerekir. Testi tek bir koşumda koşturmak yetmez; aynı testi yinelemeyle çok kez koşturup kaç kez kaldığını saymak gerekir. Bu derste sekiz koşum ve hazır olma turlarının dizisi bu yinelemenin modelidir. Sekiz koşumun ikisinde kalma, ekip için bir kırılganlık işaretidir.
Ölçüm, onarımın etkisini de gösterir. Onarımdan sonra aynı sayıda koşum yinelenir ve kalan koşum sayısı sıfırsa onarım tutmuştur. Ama sıfır kalan koşum, test kararlıdır anlamına gelmez; yalnız bu koşumlarda kırılmadı anlamına gelir. Koşum sayısı arttıkça kanıt güçlenir. Bu yüzden onarım öncesi ve sonrası aynı sayıda koşumla karşılaştırılır.
Kırılgan Testle Ne Yapılır
Kırılgan bir test bulunduğunda üç yol vardır. İlki testi karantinaya almaktır: sonuç raporlanır ama hattı durdurmaz. Karantina geçici bir önlemdir ve bir son tarihle birlikte konur. İkincisi kök nedeni bulup onarmaktır: zamanlamayı koşula bağlamak, paylaşılan durumu temizlemek, bağımlılığı sahtelemek. Üçüncüsü testi silmektir; test kimsenin güvenmediği bir sonuç veriyorsa, hiç sonuç vermemesi daha dürüsttür.
Karantina sürekli bir çözüm olursa test paketi yavaş yavaş gürültüyle dolar ve ekip kırmızı sonuçlara alışır; alışılmış bir kırmızı ise gerçek bir hatayı da gizler. Ekip karantinadaki test sayısını da bir bütçeyle izleyebilir; bu kursun ölçüsü tam bunu sorar.
Yaygın Yanılgılar
İlk yanılgı, “kırılgan testi yeniden denemek sorunu çözer” düşüncesidir. Yeniden deneme testi geçirir, ama aşımı gizler: bir koşumda iki kez deneyen test, iki koşumun maliyetini öder ve kırılganlığı raporlardan siler. Bu derste ölçülen şey tam olarak bu gizlenen aşımdır.
İkinci yanılgı, “kırılgan testin sorumlusu testtir” düşüncesidir. Zamanlamayı belirsiz bırakan uygulama kodu da kırılganlığın nedeni olabilir. Uygulama hazır olduğunu bildirmiyorsa, test ne kadar iyi yazılırsa yazılsın tahmin yürütmek zorunda kalır. Boşta kaynağı bu iki tarafı birbirine bağlar.
Üçüncü yanılgı, “bekleme süresini bir kez ayarlayıp unutmak yeterlidir” düşüncesidir. Dokuz turluk bekleme bugün tüm koşumları geçirebilir; yarın donanım ya da uygulama değişir ve onuncu turda hazır olan bir koşum çıkar. Sabit değerler eskir, koşula bağlı bekleme eskimez.
Sınırlayıcı Ölçüm
Bu ölçümün sınırlayıcısı koşula bağlı yoklamadır. Gerekçesi şudur: sabit beklemede iki kötü seçenek vardı ve ikisi de tahminden doğuyordu. Tahmini koşulla değiştirmek, hazır olma turlarını ve bütçeyi hiç değiştirmeden aynı koşumları yeniden ölçmeyi sağlar. Bütçe bu kez en kötü gözlenen tura konur; çünkü koşula bağlı beklemede amaç aşımı tahmin etmek değil, gözlenen en kötü durumun içinde kalmaktır.
Üçüncü çare, beklemeyi tahmine değil koşula bağlamaktır. Test her turda ekranın hazır olup olmadığına bakar ve hazır olunca ilerler; boşta kaynağı bunu platformda yapar. Bu durumda harcanan tur, o koşumun hazır olma turuna eşittir. Bütçe de başarısızlığa göre değil, gözlenen en kötü hazırlık turuna göre konur: 9 tur.
// Main.kt — kosula bagli yoklama
fun main() {
val runs = (0 until 8).toList()
val spent = runs.map { pollRounds(it) }
println("harcanan turlar: $spent")
val failed = runs.count { !passesAfterFixedWait(it, pollRounds(it)) }
println("kalan kosum: $failed")
val overrun = Overrun("kosula-bagli-yoklama", Budget("tur", 9), spent)
overrun.line()
countOverruns(listOf(overrun))
}
harcanan turlar: [3, 4, 3, 8, 4, 3, 9, 4] kalan kosum: 0 kosula-bagli-yoklama: butce=tur<=9 | asan=0/8 -> icinde icinde: 1 azinda: 0 cogunda: 0 hepsinde: 0
Her koşum kendi hazır olma turunda bitti, hiçbir koşum kalmadı ve ölçüm bütçenin içinde kaldı. Önceki iki çarenin ödediği bedeli bu çare ödemedi: hızlı koşumlar kısa bekledi, yavaş koşumlar yavaş ekranı bekledi.
Bu sonucun sınırı bütçededir. 9 turluk bütçe gözlenen en kötü turdan geliyor. Onuncu turda hazır olan bir koşum ortaya çıkarsa ölçüm yeniden yapılmalıdır. Koşula bağlı yoklama kırılganlığı yok etmez; kırılganlığı üreten düzeneği, yani tahmini kaldırır.
Özet
- Bu dersin aşımı: azında. Beş tur bekleyen test, sekiz koşumun ikisinde hazır olma turuna yetişemedi.
- Kırılganlık bir aşım kategorisi değil, aşımı üreten düzenektir; ölçüm yalnız aşımın kaç koşuma yayıldığını sayar.
- Beklemeyi dokuza çıkarmak kalan koşumu sıfırladı ama harcanan turu her koşumda bütçenin üstüne taşıdı: hepsinde.
- Koşula bağlı yoklama her koşumu kendi hazır olma turunda bitirdi ve 9 turluk bütçenin içinde kaldı.
- Boşta kaynağı, uygulamanın meşgul olup olmadığını teste bildirir; test süreyi tahmin etmez, uygulamaya sorar.
Sonraki Adım
Test yazıldı, kararlı hâle getirildi ve koşuyor. Ama kodun kendisi hakkında test çalıştırmadan da bir şeyler söylenebilir. Sıradaki ders, Statik Çözümleme, bir otomatik denetleyicinin modül başına bulduğu kural ihlallerinin bütçeyi kaç modülde aştığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.