Ders 01 / 19
Birim Testleri
Birim testi saf Kotlin katmanındaki bir işlevin ürettiği sayıyı bir girdi kümesi için sabitler. Ders bir ölçümün bütçesini kaç örnekte aştığını dört yanıtla sınıflar: içinde, azında, çoğunda, hepsinde.
Yayın Güncelleme
Bu derste
- Bir ölçümün bütçesini kaç örnekte aştığını içinde, azında, çoğunda ve hepsinde olarak sınıflar
- Birim testinin bir girdi için değil bir girdi kümesi için sayı sabitlediğini açıklar
- Bir işlevin en kötü durumuna göre bütçe koyar ve bütçeyi aşan örneği sayar
- Aynı işlevin bütçe değişince başka bir aşım verdiğini ölçer
İçindekiler
Önceki kurs bir olayın alıcıya kaç kopya ulaştığını saydı. O kursun ölçü ekseni burada sürmez. Bu kursta uygulama artık üretime götürülüyor ve üretime giden her adım bir ölçümü bir sınırla karşılaştırıyor: birim testi bir işlevin attığı adımları, aygıt üstü test bir bekleme döngüsünün tur sayısını, başlangıç ölçümü ilk ekrana kadar yapılan işi, aşamalı yayın her aşamadaki çökmeleri. Tek bir ölçüm yanıltır, çünkü tek bir girdide her şey yolunda görünebilir. Bu yüzden aynı ölçüm birden çok girdide alınır ve her girdi bir örnek olur.
Kursun tek sorusu bu dersten başlar: bu ölçüm bütçesini kaç örnekte aşıyor? Bütçe, bir ölçümün aşmaması beklenen sınırdır. Yanıtın dört biçimi var ve kurs boyunca adları değişmez. Hiçbir örnek bütçeyi aşmıyorsa ölçüm içindedir. Aşan örnekler var ama örneklerin yarısından azıysa azındadır. Örneklerin en az yarısı aşıyor ama hepsi aşmıyorsa çoğundadır. Her örnek aşıyorsa hepsindedir. Dört yanıtın üst adı aşımdır; bir ölçümün aşımı denince bu dört yanıttan biri anlaşılır.
Ölçünün Kuralı
Aşımı etiket değil kural verir. Aşağıdaki çekirdek bir bütçeyi (Budget), bir ölçümün etiketini, bütçesini ve örnek listesini (Overrun) tanımlar. Örnek sayısını ve sınırı kesin olarak aşan örneklerin sayısını alır, sonra soruyu sırayla sorar. Sınıra eşit bir örnek aşım sayılmaz. Tam yarı da çoğunda sayılır: sekiz örnekten dördü aşıyorsa ölçüm azında değil çoğundadı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 }}")
}
}
countOverruns dört aşımı sabit sırayla sayar ve sıfır olan aşımı da basar. Kural iki şeyi görmez. Birincisi büyüklüktür: bütçeyi bir adımla aşan örnek ile on adımla aşan örnek aynı sayılır. İkincisi nedendir: yavaş bir algoritma, fazla bir çağrı ve fazla sıkı konmuş bir bütçe aynı aşımı verebilir. Büyüklüğü ve nedeni derste düzyazı söyler, kural söylemez.
Bir örnek listesini elle yazmak kuralı gösterir ama bir şey ölçmez. Bu kursta aşımı taşıyan her ölçüm, örneklerini bir model işlevinin dönüşünden alır: işlev bir girdiden bir iş birimi üretir ve örnek listesi bu işlevin her girdi için döndürdüğü sayılardır. İş birimi dersin işine göre adım, tur, istek ya da çökme olur; süre değildir. Bütçe de modelin girdisidir: derste bir bütçe koyarsak gerekçesini söyleriz ve onu bir platform eşiği gibi sunmayız.
TA1. Model, test çerçevesinin kendisini koşturmaz; bu ortamda çerçeve yoktur. Modelde bir testin ölçtüğü şey, bir işlevin bir girdi için attığı adım sayısıdır. Test sınıfı ise platform parçasıdır ve çıktı vermez.
Birim Testi Neyi Sabitler
Birim testi (unit test), uygulamanın saf Kotlin katmanındaki bir işlevi çalıştırır ve sonucunu bir beklentiyle karşılaştırır. Çalıştırılan işleve test hedefi (system under test), beklentiyi dile getiren çağrıya iddia (assertion) denir. Saf Kotlin katmanı ekranı, dosyayı, ağı ve saati bilmez; işlev aynı girdiyle her zaman aynı sonucu verir. Bu yüzden test aygıt ya da öykünücü olmadan, geliştirme makinesinde, çok kısa sürede koşar.
Bir iddia tek bir girdi için tek bir sayıyı sabitler. İşlevin davranışı ise girdiye göre değişir: bir girdide iki adım atan işlev başka bir girdide on adım atabilir. Bir girdi kümesi seçildiğinde birim testi, o kümedeki her girdi için sayıyı sabitleyen iddialar topluluğuna dönüşür. Platformda bu şöyle görünür.
// Platform kodu: ValidatorTest
import org.junit.Assert.assertEquals
import org.junit.Test
class ValidatorTest {
@Test
fun acceptsShortCode() {
assertEquals(4, validateSteps("AB12"))
}
@Test
fun stopsAtFirstBrokenCharacter() {
assertEquals(2, validateSteps("A-1234"))
}
}
Her @Test yöntemi bir girdiyi sabitler. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde iddia yerine kurala bakarız: örneklerin kaçı bütçeyi aşıyor.
Bu sorunun önceki kurslardaki iki soruyla karışması kolaydır. M30/K06’da bir test ile bir sürüm çiftini karşılaştırıp bu test doğru sürümü kusurlu sürümden ayırıyor mu diye sormuştuk. Orada ölçülen şey testin doğruluğuydu ve her ölçüm bir test ile bir sürümden oluşan bir çifte dayanıyordu. Burada çift yoktur: tek bir ölçüm, tek bir bütçe ve bir örnek listesi vardır. Soru testin doğruluğu değildir, bütçenin kaç kez aşıldığıdır.
Sekiz Örnek, Dört Arama
Bir uygulamanın saf Kotlin katmanında ürün kodu araması olsun. Kodlar 1’den 16’ya sıralı durur ve sekiz kod aranır. Dört arama yöntemi aynı bütçeyle ölçülür: bir arama en fazla dört adım atabilir. Dört adımı bir platform eşiği gibi sunmuyoruz; ürün ekranının bir aramaya ayırdığı payın modeldeki karşılığı olarak koyuyoruz.
scanSteps baştan tarar ve aranan kodun sırası kadar adım atar. halveSteps aralığı her adımda yarıya indirir. filterSteps aranan kodu bulsa da bütün listeyi dolaşır. bucketSteps kodu sekize bölümden kalanına göre bir kovaya ayırır ve yalnız o kovada arar. Her işlev aranan kodu bulmak için attığı adımı döndürür.
// Main.kt
fun scanSteps(items: List<Int>, target: Int): Int {
var steps = 0
for (item in items) {
steps++
if (item == target) return steps
}
return steps
}
fun halveSteps(items: List<Int>, target: Int): Int {
var low = 0
var high = items.size - 1
var steps = 0
while (low <= high) {
steps++
val mid = (low + high) / 2
when {
items[mid] == target -> return steps
items[mid] < target -> low = mid + 1
else -> high = mid - 1
}
}
return steps
}
fun filterSteps(items: List<Int>, target: Int): Int {
var steps = 0
items.filter { steps++; it == target }
return steps
}
fun bucketSteps(items: List<Int>, target: Int): Int =
items.filter { it % 8 == target % 8 }.indexOf(target) + 1
fun main() {
val codes = (1..16).toList()
val targets = listOf(2, 5, 7, 9, 12, 14, 16, 3)
println("aranan kodlar: $targets")
val step = Budget("adim", 4)
val overruns = listOf(
Overrun("dogrusal-arama", step, targets.map { scanSteps(codes, it) }),
Overrun("ikili-arama", step, targets.map { halveSteps(codes, it) }),
Overrun("tam-suzme", step, targets.map { filterSteps(codes, it) }),
Overrun("kovali-arama", step, targets.map { bucketSteps(codes, it) }),
)
overruns.forEach { it.line() }
countOverruns(overruns)
}
aranan kodlar: [2, 5, 7, 9, 12, 14, 16, 3] dogrusal-arama: butce=adim<=4 | asan=6/8 -> cogunda ikili-arama: butce=adim<=4 | asan=1/8 -> azinda tam-suzme: butce=adim<=4 | asan=8/8 -> hepsinde kovali-arama: butce=adim<=4 | asan=0/8 -> icinde icinde: 1 azinda: 1 cogunda: 1 hepsinde: 1
Dört ölçümün her biri farklı bir aşım verdi: içinde 1, azında 1, çoğunda 1, hepsinde 1. Bu dağılım kursun bağlayıcı sayısıdır; örnek sayısı 8, bütçe 4 adımdır. Her ölçümün örnekleri, ilgili işlevin sekiz girdi için döndürdüğü listedir.
dogrusal-arama çoğundadır. 5, 7, 9, 12, 14 ve 16 kodlarının sırası dörtten büyüktür, 2 ve 3 ise sınırın altında kalır. Altı örnek sekizin yarısından fazladır. ikili-arama azındadır: on altı kodun en fazla beş adım gerektirdiği tek yer son koddur, yani yalnız 16 aşar. tam-suzme her örnekte 16 adım atar ve hepsindedir; aranan kod ilk sıralarda olsa da filtre listeyi sonuna kadar dolaşır. bucketSteps her kovaya iki kod koyar, bu yüzden hiçbir örnek iki adımı geçmez ve ölçüm içindedir.
Çoğunda ile hepsinde arasındaki fark sayıda değil, çıkarımdadır. Çoğundaysa bütçe yalnız bazı girdiler için tutmuyor; girdi seçimi önemli. Hepsindeyse hiçbir girdi bütçeyi tutturamıyor; işlevin kendisi bütçenin dışındadır.
Girdi Doğrulama: En Kötü Duruma Göre Bütçe
Birim testi yalnız arama için değil, her saf işlev için kurulur. Bir kampanya kodunu doğrulayan işlevi ele alalım. İşlev kodun karakterlerini sırayla gezer ve geçersiz bir karakter görür görmez durur. Bir kod en fazla altı karakterlidir; yani işlevin en kötü durumu altı adımdır. Bütçeyi bu en kötü duruma koymak, işlevin hiçbir girdide kendi sınırını aşmadığını sabitlemenin yoludur. İşlev bu derste yardımcı bir dosyada durur ve sonraki ölçümler onu çağırır.
// Validation.kt — kampanya kodunun karakterlerini sirayla gezer, gecersiz karakterde durur
fun validateSteps(code: String): Int {
var steps = 0
for (ch in code) {
steps++
if (!ch.isLetterOrDigit()) return steps
}
return steps
}
Sekiz kod seçtik. Kodlardan biri kısa, ikisi hemen başında bozuk, üçü altı karakterin tamamını gezecek kadar uzun. Kodların listesi aynı kalacak; yalnız bütçe değişecek.
// Main.kt — girdi dogrulamanin butcesi en kotu duruma gore
fun main() {
val codes = listOf("AB12", "A-1234", "YAZ24", "123456", "K 9", "TR2026", "ZZ", "P7Q8R9")
val steps = codes.map { validateSteps(it) }
println("denenen kodlar: $codes")
println("adimlar: $steps")
val worst = Budget("adim", 6)
val overrun = Overrun("kod-dogrulama", worst, steps)
overrun.line()
countOverruns(listOf(overrun))
}
denenen kodlar: [AB12, A-1234, YAZ24, 123456, K 9, TR2026, ZZ, P7Q8R9] adimlar: [4, 2, 5, 6, 2, 6, 2, 6] kod-dogrulama: butce=adim<=6 | asan=0/8 -> icinde icinde: 1 azinda: 0 cogunda: 0 hepsinde: 0
Sekiz kodun adımları 4, 2, 5, 6, 2, 6, 2 ve 6 çıktı. Hiçbiri altıyı geçmedi, ölçüm içindedir. Altı adım atan üç kod bütçenin tam sınırındadır ve sınıra eşit örnek aşım sayılmaz. Bu ölçümün değeri bir sayı değil, bir sözleşmedir: işlev gelecekte değişir ve bir kod yedi adım atarsa, bu ölçüm o ana kadar içinde diyen bir testin kırılmasına yol açar. Birim testi bu yüzden bir kez yazılıp unutulmaz; her değişiklikte aynı sekiz girdi aynı bütçeyle yeniden ölçülür.
TA2. Bütçe olan 6 adım, bu işlev için en uzun kodun karakter sayısından türer. Platformda ürün ekibi başka bir en kötü durum seçebilir; model bu seçimi bir bütçe olarak koyar ve onu bir eşik iddiasına çevirmez.
Yaygın Yanılgılar
Birim testi etrafında üç yanılgı sürekli tekrar eder ve ölçüm her birine somut bir karşılık verir.
Birincisi, “test yeşilse işlev doğrudur” düşüncesidir. Yeşil test yalnız seçilen girdiler için sabitlenen iddiaların tuttuğunu söyler. Girdi kümesi dar seçilmişse en kötü durum kümenin dışında kalabilir. Bu derste bütçeyi en uzun kodun adım sayısına göre koymamızın nedeni budur: bütçe kümenin en zorlu üyesine dayanmazsa, içinde yanıtı bir şey söylemez.
İkincisi, “bütçe ne kadar sıkıysa o kadar iyi” düşüncesidir. Sıkı bir bütçe aşımı artırır, ama aşım sayısı yükseldi diye işlev kötüleşmez; yalnız sınır değişmiştir. Aynı işlev 6 adımlık bütçede içinde, 5 adımlık bütçede azında çıktı. İşlev aynıydı. Ölçülen şey işlevin kendisi değil, işlevle sınırın ilişkisiydi. Bu yüzden bir bütçe değeri her zaman bir gerekçeyle birlikte yazılır ve gerekçe değişince bütçe de gözden geçirilir.
Üçüncüsü, “birim testi yalnız hata arar” düşüncesidir. Burada test bir hatayı değil bir sayıyı sabitliyor. Sayı bir sonraki değişiklikte kımıldarsa test kırılır ve ekip bu değişimi fark eder. Hatasız bir işlevde bile adım sayısının sessizce artması, bütçeye yaklaşıldığının ilk işaretidir; test bu işareti yakalamak için vardır.
Üç yanılgının ortak yanı tek bir sonuçtan fazlasını okumaktır. Kural yalnız şunu söyler: sınır kesin olarak kaç örnekte aşıldı. Bunun ötesi, yani aşımın neden olduğu, ekibin işidir.
Sınırlayıcı Ölçüm
Bütçeyi sıkılaştıralım. İşlev değişmedi, kod listesi değişmedi; yalnız bütçe 6 adımdan 5 adıma indi. Aynı sekiz örnek yeni sınırla karşılaştırılacak.
// Main.kt — ayni islev, ayni kodlar, daha siki butce
fun main() {
val codes = listOf("AB12", "A-1234", "YAZ24", "123456", "K 9", "TR2026", "ZZ", "P7Q8R9")
val steps = codes.map { validateSteps(it) }
val tight = Budget("adim", 5)
val overrun = Overrun("kod-dogrulama-siki", tight, steps)
overrun.line()
countOverruns(listOf(overrun))
}
kod-dogrulama-siki: butce=adim<=5 | asan=3/8 -> azinda icinde: 0 azinda: 1 cogunda: 0 hepsinde: 0
Aynı işlev, aynı sekiz girdi ve aynı adımlar: 4, 2, 5, 6, 2, 6, 2, 6. Bütçe 5’e inince altı adım atan üç kod aşar. Sekiz örnekten üçü aşıyor ve üç, yarının altında kaldığı için ölçüm azındadır. Sayı bir önceki ölçümde sıfırdı. İşlevde tek bir satır değişmedi.
Bu bir çelişki değil, ölçünün kendisidir. Aşım yalnız işlevin değil, işlev ile bütçenin birlikte bir özelliğidir. Aynı işlev bir bütçede içinde, başka bir bütçede azında, daha da sıkı bir bütçede çoğunda ya da hepsinde olabilir. Bu yüzden bir ölçümün aşımı söylenirken bütçe de söylenir: “kod doğrulama içinde” tek başına bir bilgi değildir, “kod doğrulama, 6 adımlık bütçeyle içinde” bir bilgidir.
Kuralın görmediği iki şey burada da geçerlidir. Büyüklüğü görmez: beş adımlık bütçeyi bir adımla aşan üç kod, kural gözünde on adımla aşan üç kodla aynıdır. Nedeni de görmez: bu ölçümde aşımın nedeni işlev değildir, çünkü işlev aynı kalmıştır; neden bütçenin daha sıkı konmasıdır. Birim testi yazan biri bir ölçümün aşımını okurken önce işleve değil, bütçenin neye göre konduğuna bakmalıdır.
Dört aramadan iki ders çıkar. Birincisi, aynı bütçe dört işlevde dört aşım verebilir: bütçenin sabit olması işlevleri ayırır. İkincisi, aynı işlev iki bütçede iki aşım verebilir: işlevin sabit olması bütçeleri ayırır. Birim testi bu iki yönün ikisinde de bir şey sabitler. Girdi kümesini sabitleyerek işlevi, bütçeyi sabitleyerek beklentiyi kilitler.
Özet
- Bu dersin aşımı: içinde. Altı adım bütçeli girdi doğrulama sekiz kodun hiçbirinde bütçeyi aşmadı.
- Dersin ölçütü, bir ölçümün bütçesini kaç örnekte aştığı: dört aramada içinde 1, azında 1, çoğunda 1, hepsinde 1 çıktı; örnek sayısı 8, bütçe 4 adımdı.
- Birim testi bir girdi için değil bir girdi kümesi için sayı sabitler; saf Kotlin katmanındaki işlev aynı girdiyle aynı sonucu verir.
- Aynı işlev ve aynı sekiz kod, bütçe 6 adımdan 5 adıma inince içinden azına geçti: aşım işlevin değil, işlev ile bütçenin özelliğidir.
- Kural aşımın sayısını görür, büyüklüğünü ve nedenini görmez; ikisini düzyazı söyler.
Sonraki Adım
Bu derste ölçülen işlevler hiçbir dış kaynağa dokunmadı. Gerçek bir depo ise ağ kaynağını çağırır ve birim testi o çağrıyı istemez. Sıradaki ders, Sahteleme, bir bağımlılığı sahtesiyle değiştirmenin çağrı sayısını bütçenin kaç örneğinde aştığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.