Ders 06 / 19
Birim Testleri
Test hedefi, iddia ve sahte bağımlılıkla birim testinin kurulması; aynı sınır denetiminin doğru ve kusurlu sürümü ayırıp ayırmadığının dört kategoride ölçümü.
Yayın Güncelleme
Bu derste
- Test hedefinin, test sınıfının ve iddianın birim testindeki ayrı rollerini ayırır
- Bir denetimin geçmesinin kodun doğru olduğunu göstermediğini ölçerek açıklar
- Sınır değerini deneyen iddia ile eşikten uzak değeri deneyen iddiayı ölçüyle karşılaştırır
- Beklenen değerin kusurlu koddan kopyalandığı durumda iddianın doğru sürümü reddettiğini gösterir
İçindekiler
Önceki derste (Modülerleştirme) bir modülün sınırını aşan bağımlılığın nasıl görünür olduğuna baktık. Bu ders sınırın başka bir türüne geçer: bir değerin kural değiştirdiği sınıra. Bir sepette ücretsiz kargo yüz birimden başlıyorsa, kodun en kolay bozulduğu yer tam yüz değeridir. Birim testi bu yerleri denemek için vardır.
Önceki kurs (Veri, Ağ ve Eşzamanlılık) bir işlemin iki koşumunu karşılaştırdı: işlem yinelenirse ya da sırası değişirse sonuç değişiyor mu. Bu kursun sorusu başkadır. Burada tek bir denetime bakarız ve onu iki sürüme uygularız: kodun doğru sürümüne ve içine bilerek bir kusur koyduğumuz sürümüne. Denetim doğru sürümde geçip kusurlu sürümde kalıyorsa işe yarıyordur. İkisinde de geçiyorsa hiçbir şey söylemiyordur.
Ölçü ekseni ayrımdır. Dört durum çıkar ve çıktıda ASCII adlarıyla görünür. Ayıran denetim doğru sürümde geçer, kusurlu sürümde kalır. Kör denetim ikisinde de geçer; kusuru göremez. Gürültülü denetim ikisinde de kalır; doğru kodu bile reddeder. Ters denetim doğru sürümde kalır, kusurlu sürümde geçer; yani kusuru ödüllendirir.
| Kategori | Doğru sürüm | Kusurlu sürüm |
|---|---|---|
| ayıran | geçer | kalır |
| kör | geçer | geçer |
| gürültülü | kalır | kalır |
| ters | kalır | geçer |
Aşağıdaki çekirdek bu dört durumu sınıflar. Etiketi biz vermeyiz; separation yalnız iki doğruluk değerinden çıkar.
// Separation.swift — bir denetimin dogru surumu kusurlu surumden ayirip ayirmadigini siniflandirir
struct Contrast<Output: CustomStringConvertible> {
let label: String
let correct: Output
let faulty: Output
let passesCorrect: Bool
let passesFaulty: Bool
var separation: String {
switch (passesCorrect, passesFaulty) {
case (true, false): return "ayiran"
case (true, true): return "kor"
case (false, false): return "gurultulu"
case (false, true): return "ters"
}
}
func describe() -> String {
let onCorrect = passesCorrect ? "gecti" : "kaldi"
let onFaulty = passesFaulty ? "gecti" : "kaldi"
print("\(label) | dogru=\(correct)/\(onCorrect) kusurlu=\(faulty)/\(onFaulty) -> \(separation)")
return separation
}
}
func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}
func countSeparations(_ results: [String]) {
for name in ["ayiran", "kor", "gurultulu", "ters"] {
print("\(name): \(results.filter { $0 == name }.count)")
}
}
Contrast iki sürümün çıktısını ve denetimin o çıktılar üzerindeki kararını tutar. contrast işlevi aynı check kapanışını (closure) önce doğru, sonra kusurlu çıktıya uygular. describe kararı tek satıra yazar. Kural nedeni: bir denetimi tek sürümde çalıştırmak ona hiçbir şey sormaz. Denetimin değeri, iki sürümü ayırabilmesindedir.
Test Hedefi, Test Sınıfı ve İddia
Bir birim testi üç parçadan oluşur. Test hedefi (test target) uygulamanın kodundan ayrı derlenen pakettir; testler orada yaşar ve uygulama kodunu içe alır. Test sınıfı bir XCTestCase altsınıfıdır; içindeki her test ile başlayan yöntem tek bir deneydir. İddia (assertion) deneyin sonucunu beklenen değerle karşılaştıran çağrıdır; tutmazsa test kalır.
Aşağıdaki örnek bir sepetin kargo ücretini denetler. Kural şudur: ara toplam yüz ya da daha fazlaysa kargo ücretsizdir, altındaysa on beş birim tutar.
// kargo ucreti testi — KAYNAK
import XCTest
final class ShippingFeeTests: XCTestCase {
func testFeeAtThreshold() {
XCTAssertEqual(shippingFee(for: 100), 0)
}
func testFeeFarAboveThreshold() {
XCTAssertEqual(shippingFee(for: 500), 0)
}
func testFeeBelowThreshold() {
XCTAssertEqual(shippingFee(for: 40), 15)
}
}
Bu parça çalıştırılmaz; test çerçevesi geliştirme ortamının test koşucusu içinde çalışır. XCTAssertEqual(_:_:) iki değeri karşılaştırır. Her yöntem bağımsız koşar; bir yöntem kalsa bile öbürleri çalışır. Değerleri hazırlamak için setUp() her yöntemden önce, tearDown() sonra çağrılır.
Aynı üç deney ikinci bir biçimde de yazılabilir. Bu biçimde yöntem yerine @Test ile işaretli bir işlev, iddia yerine #expect(_:) makrosu kullanılır.
// kargo ucreti testi, ikinci bicim — KAYNAK
import Testing
@Test func feeAtThreshold() {
#expect(shippingFee(for: 100) == 0)
}
@Test func feeFarAboveThreshold() {
#expect(shippingFee(for: 500) == 0)
}
#expect iddiası tutmadığında testi durdurmaz; sonraki iddialar da koşar. Durdurması gerekiyorsa #require(_:) kullanılır. Eski biçimde #expectin karşılığı XCTAssertTrue(_:), #requireın karşılığı XCTUnwrap(_:) çağrısıdır. İki biçim aynı soruyu sorar; bu derste yalnız bu ortak soruyu ölçeriz.
Kargo Ücreti Modeli
Aşağıdaki model, denetimin iki sürüme ne dediğini kurar; test çerçevesinin kendisi değildir. Modelde shippingFee(for:) iki sürümle vardır. Doğru sürüm eşikte ve üstünde sıfır döndürür. Kusurlu sürümde karşılaştırma >= yerine > yazılmıştır, yani tam yüzde ücret hâlâ alınır. Bu, birim testlerinin en bilinen kusurudur: bir sınırın bir yanı yanlış sayılır.
// Fees.swift — kargo ucretinin dogru ve kusurlu surumu ile denetim yardimcisi
func correctFee(for subtotal: Int) -> Int {
subtotal >= 100 ? 0 : 15
}
func faultyFee(for subtotal: Int) -> Int {
subtotal > 100 ? 0 : 15
}
func feeCheck(_ label: String, subtotal: Int, expected: Int) -> String {
contrast(label, correct: correctFee(for: subtotal), faulty: faultyFee(for: subtotal), check: { $0 == expected }).describe()
}
feeCheck yukarıdaki iddialardan birinin modelidir: iki sürümden aynı ara toplamla ücret ister ve XCTAssertEqual(…, expected) yerine $0 == expected karşılaştırmasını yapar. Etiketler testlerin adlarının Türkçe ASCII karşılığıdır. Üç gerçek deneyin yanına dördüncü bir deney ekledik: beklenen değeri yanlış yazılmış bir test. Böylece üç kategori birlikte görünür.
// main.swift — dort denetim, iki surum
let atThreshold = feeCheck("esikte-ucret", subtotal: 100, expected: 0)
let farAbove = feeCheck("cok-ustunde-ucret", subtotal: 500, expected: 0)
let below = feeCheck("altinda-ucret", subtotal: 40, expected: 15)
let wrongSpec = feeCheck("yanlis-beklenen", subtotal: 40, expected: 10)
countSeparations([atThreshold, farAbove, below, wrongSpec])
esikte-ucret | dogru=0/gecti kusurlu=15/kaldi -> ayiran cok-ustunde-ucret | dogru=0/gecti kusurlu=0/gecti -> kor altinda-ucret | dogru=15/gecti kusurlu=15/gecti -> kor yanlis-beklenen | dogru=15/kaldi kusurlu=15/kaldi -> gurultulu ayiran: 1 kor: 2 gurultulu: 1 ters: 0
esikte-ucret ayıran: doğru sürüm sıfır döndürür ve denetim geçer; kusurlu sürüm on beş döndürür ve denetim kalır. cok-ustunde-ucret ile altinda-ucret kör: ikisi de iki sürümde geçer, çünkü iki sürüm yüz değerinden uzakta aynı davranır. yanlis-beklenen gürültülü: beklenen değer on olduğu için doğru sürüm bile kalır.
Burada dikkat çeken şey, üç testin de yeşil görünebilecek olmasıdır. Üç gerçek denetimden ikisi kusurlu sürümde de geçti. Sınır değerini denemeyen bir test takımı kusuru taşıyan kodu yeşil raporlayabilir. Bu, kapsamın ve test sayısının tek başına güvence vermediğinin ilk işaretidir; kapsamı üçüncü derste ayrıca ölçeceğiz.
TS1. Bu ölçüm tek bir kusuru, >= yerine > yazılmasını denedi. Kod başka biçimde bozulsaydı, örneğin eşik yüz yerine doksan dokuz olsaydı, hangi denetimin ayırdığı değişirdi.
TS2. Modelin faultyFee işlevi kusuru biz koyduğumuz için biliyoruz. Gerçek bir kodda kusurun nerede olduğu önceden bilinmez; bu yüzden ayrım ölçüsü, kusuru bilinen bir bozma denemesiyle (kodu bilerek bozup testin tepki verdiğine bakmak) birlikte anlamlıdır.
Sınır Değeri Neden Ayırır
Ayıran denetimle kör denetim arasındaki fark bir kural gibi okunabilir: kuralın değiştiği değeri dene. Eşik yüzse sınır değerleri doksan dokuz, yüz ve yüz birdir. Yüz, kuralın iki tarafını ayırdığı için en çok bilgi taşır. Bir >= ile > farkı, yalnız bu değerde ortaya çıkar; başka hiçbir girdi iki sürümü birbirinden ayıramaz.
Kör denetimler değersiz değildir. altinda-ucret ücretin on beş olduğunu söyler ve bu bir davranış sözleşmesidir; ileride biri ücreti değiştirirse test yakalar. Ama kusuru bulmak isteyen bir denetim olarak kördür. İki rol ayrıdır: bir test davranışı sabitleyebilir, yine de belirli bir kusuru ayıramaz. Ayrım ölçüsü ikincisine bakar.
Bir test yazarken kendine sorulacak soru şudur: bu denetimi kusurlu sürüme uygulasam kalır mıydı. Cevap evetse denetim ayırır. Hayırsa kör olduğunu bilerek yazarsın. Bu soruyu cevaplamanın en ucuz yolu, testi yazdıktan sonra koddaki karşılaştırmayı bir dakikalığına bozup testin kızıp kızmadığına bakmaktır.
Sahte Bağımlılıklar
Gerçek bir sepet kodu eşiği ve ücreti kendisi hesaplamaz; bir fiyat servisinden, bir saatten ya da bir ağ istemcisinden alır. Birim testi bu bağımlılıkları gerçeğiyle çalıştırmaz, çünkü test hızlı ve belirlenimli olmalıdır. Bağımlılık enjeksiyonu (dependency injection) bağımlılığı dışarıdan vermektir; test de yerine bir sahte bağımlılık koyar.
Sahte bağımlılığın üç yaygın biçimi vardır. Güdük hazır bir cevap döndürür. Casus nesne çağrıldığını kaydeder, sonra çağrıya bakılır. Kukla nesne yalnız bir yeri doldurur ve hiç çağrılmaz. Bu üçüne topluca sahte nesne de denir.
// sahte bagimlilik — KAYNAK
protocol RateProviding {
var freeShippingThreshold: Int { get }
}
struct StubRates: RateProviding {
let freeShippingThreshold = 100
}
final class SpyReceipt {
private(set) var printedTotals: [Int] = []
func print(total: Int) { printedTotals.append(total) }
}
StubRates güdüktür: eşiği sabit bir değer olarak verir ve test eşiğin yüz olduğundan emin olur. SpyReceipt casus nesnedir: fişe hangi toplamların yazıldığını saklar ve test sonradan printedTotals dizisine bakar. Bu parça da KAYNAK kalır; yalnız tür bildirimleridir, ölçülecek bir çıktısı yoktur.
Sahte bağımlılığın sınırı şudur: güdük sabit bir değer verdiğinde, kodun gerçek bağımlılıkla davranışı ölçülmez. Eşik yüz olarak sabitlendiği için eşiğin sunucudan yanlış geldiği bir kusur bu testten çıkmaz. Birim testi kodun mantığını ölçer; bileşenlerin birlikte çalışmasını başka bir test katmanı ölçer.
Sınırlayıcı Ölçüm
Birim testinin en sinsi kusuru beklenen değerin kendisindedir. Bir test yazılırken beklenen değer, test kâhini (test oracle) denen bir kaynaktan gelir: şartname, elle yapılmış bir hesap ya da başka bir bağımsız yol. Bazen kâhin yoktur. Yazan kişi kodu çalıştırır, çıkan değeri görür ve iddiaya yapıştırır. Değer o anda geçer, çünkü iddia kodun kendi çıktısıdır.
Aşağıdaki ölçüm bunu yapar. Sınır değerini deneyen aynı esikte-ucret iddiası alınır ama beklenen değer kusurlu sürümün çıktısından kopyalanır.
// main.swift — beklenen deger kusurlu koddan kopyalandi
let pasted = faultyFee(for: 100)
let copied = feeCheck("yapistirilmis-beklenen", subtotal: 100, expected: pasted)
countSeparations([copied])
yapistirilmis-beklenen | dogru=0/kaldi kusurlu=15/gecti -> ters ayiran: 0 kor: 0 gurultulu: 0 ters: 1
Sınır değerini deneyen aynı iddia önceki ölçümde ayırandı. Şimdi ters: doğru sürüm kalır, kusurlu sürüm geçer. Denetimin kendisi değişmedi; değişen, beklenen değerin nereden geldiğidir. Kusurlu kodun çıktısı kâhin yapıldığında test kusuru onaylar ve ileride kod düzeltildiğinde kırmızıya döner. Takım testi suçlar, kusuru değil.
Bu yüzden ayrım yalnız denetimin ne yaptığına bağlı değildir; beklenen değerin kaynağına da bağlıdır. İyi bir kâhin kodu okumadan önce vardır: sepet şartnamesi, elle hesaplanmış bir örnek ya da önceki sürümün belgelenmiş çıktısı.
TS3. Bu ölçüm kopyalamayı kusurlu sürümden yapar. Doğru sürümden kopyalanan bir değer iddiayı ayıran yapardı; ama bu durumda da değer ancak doğruluğu önceden biliniyorsa güvenilirdir.
TS4. Ölçüm tek bir sınırı, yüzü, denetler. Üç sınır değerinin hepsi aynı ters sonucu verirdi, ama bunu ayrıca ölçmedik.
Sık Yapılan Hatalar
- Yalnız eşikten uzak değerleri denemek. Bu denetimler kör kalır.
- Beklenen değeri kodu çalıştırıp kopyalamak. Kâhin kodun kendisi olursa test kusuru onaylar.
- Bir testte birden çok davranışı denetlemek. Test kalınca hangi davranışın bozulduğu anlaşılmaz.
- Sahte bağımlılığı gerçeğe benzetmeye çalışıp testi karmaşıklaştırmak. Güdük yalnız testin ihtiyacı kadar davranmalıdır.
- Testi hiç kırmızı görmeden yeşil kabul etmek. Bir test en az bir kez bilerek bozulmuş kodda kalmış olmalıdır.
Özet
- Birim testi test hedefinde yaşar; test sınıfındaki her yöntem tek bir deneydir ve iddia sonucu beklenen değerle karşılaştırır.
- Bir denetimin geçmesi kodun doğru olduğunu göstermez; ayrım, denetimin doğru sürümü kusurlu sürümden ayırıp ayırmadığıyla ölçülür.
- Dört denetimden biri ayıran, ikisi kör, biri gürültülü çıktı; sınır değerini deneyen denetim ayıran tek denetimdi.
- Beklenen değer kusurlu koddan kopyalandığında aynı denetim ters oldu: doğru sürümü reddetti, kusurlu sürümü kabul etti.
- Sahte bağımlılıklar testi hızlı ve belirlenimli tutar ama gerçek bağımlılıkla davranışı ölçmez.
- Bu dersin ayrımı: ayıran.
Sonraki Adım
Birim testi kodun mantığını sınar; kullanıcının gördüğü ekranı sınamaz. Sıradaki derste Arayüz Testleri, bir düğmeye basan sürücünün hangi denetimlerinin kusuru bulduğunu, hangilerinin yalnız metin değişince kırıldığını ölçecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.