İçeriğe geç
academia.sh

Ders 03 / 09

Test Çerçeveleri

Aynı testler neden bazen düşer? Ders bir test çerçevesinin üç parçasını saf Ruby ile kurar, tohumun sırayı seçtiğini ve paylaşılan durumun testi düşürdüğünü ölçer.

Yayın Güncelleme

Bu derste

  • Test çerçevesinin üç parçasını (tanım, iddia, koşucu) ayırır ve test sınıfı ile tanım bloğu biçiminin koşucuya aynı tabloyu verdiğini gösterir
  • Koşucunun testleri tohumla karıştırdığını ve düşen testin tohumla yeniden üretildiğini gösterir
  • Kararsız testin kaynağının paylaşılan durum olduğunu ölçer
  • Yalıtımın sınıfı sıradan koda geçirdiğini ve kuralın yalnız iki tohumu denediğini ayırır
İçindekiler

Bağımlılık kilitlendi ve kurulan sürüm artık sabit. Yine de aynı kod, aynı kilit dosyası ve aynı testler bir koşumda geçip ötekinde düşebilir. Bu dersin gözlemi test sonucu satırıdır: tabanda [], yani hiçbir test düşmez, ama kanıt sütununda B- S+ O- görünür. Sonuç, sıra değişince değişir.

M10/K05’te bir güvencenin sınırı soruldu: bir test neyi yakalar, neyi kaçırır. Burada soru başkadır. Bir testin geçip geçmediğini ne belirliyor? Orada sınır güvencenin kapsamıydı; burada sınır, testin kendi dışındaki bir girdidir. Çekirdeğin belirleyen.rb dosyası birebir yeniden verilir; blok kaynaktır ve require_relative ile çekilir.

# belirleyen.rb — bir kosum modeli ve bir sonucu neyin belirledigini siniflandiran yardimci

# Kosum: sonucun bagli olabilecegi uc girdi. Bu uc girdinin disinda kalan her sey koddur.
Kosum = Data.define(:kilit, :tohum, :ortam)

TABAN = Kosum.new(
  kilit: { "bicimleyici" => "1.4.2", "ayristirici" => "2.0.1" },
  tohum: 1,
  ortam: { saat: Time.utc(2026, 10, 16, 9, 0), cekirdek: 4, degiskenler: {} }
)

# Her degisken tek bir girdiyi degistirir; otekiler tabandaki gibi kalir.
DEGISKENLER = {
  bagimlilik: TABAN.with(kilit: { "bicimleyici" => "1.5.0", "ayristirici" => "2.1.0" }),
  sira: TABAN.with(tohum: 2),
  ortam: TABAN.with(ortam: { saat: Time.utc(2027, 3, 1, 9, 0), cekirdek: 8,
                             degiskenler: { "ISCI_SAYISI" => "3" } })
}

# Kural: hesap once tabanda, sonra her degiskende bir kez kosar. Sonucu degistiren ilk
# degisken (bagimlilik, sira, ortam sirasiyla) belirleyendir; hicbiri degistirmiyorsa kod.
def belirleyen(hesap)
  taban = hesap.call(TABAN)
  degisti = DEGISKENLER.to_h { |ad, kosum| [ad, hesap.call(kosum) != taban] }
  tur = degisti.key(true)&.to_s || "kod"
  kanit = degisti.map { |ad, d| "#{ad[0].upcase}#{d ? '+' : '-'}" }.join(" ")
  [tur, kanit, taban]
end

Çerçevenin Üç Parçası

Bir test çerçevesi üç parçadan oluşur. Test tanımı, çalıştırılabilir bir deneme olarak adlandırılmış bir bloktur. İddia, beklenen ile gerçeği karşılaştırır ve tutmuyorsa hata yükseltir. Test koşucusu, tanımları çalıştırır, iddia hatasını yakalar ve sonucu bir tabloya yazar. Yaygın test çerçeveleri de bu üç parça üzerine kuruludur. Burada üçünü de saf Ruby ile kuruyoruz, çünkü gözlem koşucunun içindedir ve koşucu görünür olmalıdır.

# cerceve.rb — test cercevesinin uc parcasi: tanim, iddia, kosucu
class IddiaHatasi < StandardError; end

# Iddia: beklenen ile gercek ayniysa sessiz kalir, degilse hata yukseltir.
def iddia(beklenen, gercek)
  raise IddiaHatasi, "iddia tutmadi" unless beklenen == gercek
end

# Kosucu: her testi calistirir, iddia hatasini yakalir, sonucu tabloya yazar.
def testleri_kos(testler)
  testler.to_h do |ad, test|
    test.call
    [ad, :gecti]
  rescue IddiaHatasi
    [ad, :dustu]
  end
end

Dosya kaynaktır. Aşağıdaki dosya üç testi koşturur. İkincisi tam sayı bölmesini yanlış bekler.

# kucuk.rb — uc parca bir araya gelir
require_relative "cerceve"

testler = {
  "toplama" => -> { iddia(5, 2 + 3) },
  "bolme" => -> { iddia(3.5, 7 / 2) },
  "ondalik" => -> { iddia(3.5, 7 / 2.0) }
}
p testleri_kos(testler)
{"toplama" => :gecti, "bolme" => :dustu, "ondalik" => :gecti}

7 / 2 tam sayı bölmesidir ve 3 verir; ikinci iddia tutmadı, koşucu hatayı yakaladı ve bolme testini :dustu olarak yazdı. Öteki ikisi geçti. Koşucunun yalnız IddiaHatasi’nı yakaladığına dikkat edin. Başka bir hata, örneğin sıfıra bölme, bir iddia sonucu değildir ve tabloya girmez.

# hata.rb — iddia hatasi olmayan hata tabloya girmez
require_relative "cerceve"

begin
  testleri_kos("sifira bolme" => -> { 1 / 0 })
rescue => e
  p e.class
end
ZeroDivisionError

Koşucu ZeroDivisionError’u yakalamadı, hata dışarı çıktı. Bu ayrım önemlidir: bir testin düşmesi ile bir testin patlaması farklı olaylardır, ve bu derste yalnız ilkini ölçüyoruz. RK13. Burada kurulan çerçeve üç parçayı gösterir; gerçek bir çerçevenin beklenti kütüphanesi, ön hazırlık ve temizlik adımları bu modelin dışındadır.

İki Yazım Biçimi

Yaygın çerçeveler aynı üç parçayı iki ayrı yazım biçimiyle sunar. Birincisinde testler bir sınıfın test_ ile başlayan yöntemleridir ve iddia bir yöntem çağrısıdır. İkincisinde testler bir tanım bloğunun içinde adlandırılan örneklerdir ve iddia gerçek değerden başlayan bir beklenti zinciridir. Aşağıdaki dosya aynı iki testi iki biçimde yazar ve ikisini de aynı koşucuya verir.

# bicimler.rb — ayni iki test, iki yazim bicimi, tek kosucu
require_relative "cerceve"

# Birinci bicim: test sinifi; test_ ile baslayan her yontem bir testtir.
class BolmeTestleri
  def test_tam
    iddia(3, 7 / 2)
  end

  def test_ondalik
    iddia(3.5, 7 / 2)
  end
end

sinif_testleri = BolmeTestleri.public_instance_methods(false).grep(/\Atest_/).sort.to_h do |ad|
  [ad.to_s, -> { BolmeTestleri.new.public_send(ad) }]
end

# Ikinci bicim: tanim blogu ve beklenti zinciri.
Beklenti = Struct.new(:gercek) do
  def olsun(beklenen)
    iddia(beklenen, gercek)
  end
end

def bekle(gercek)
  Beklenti.new(gercek)
end

class Tanim
  attr_reader :testler

  def initialize(konu)
    @konu = konu
    @testler = {}
  end

  def ornek(ad, &blok)
    @testler["#{@konu} #{ad}"] = blok
  end
end

def tanimla(konu, &govde)
  tanim = Tanim.new(konu)
  tanim.instance_eval(&govde)
  tanim.testler
end

blok_testleri = tanimla("bolme") do
  ornek("tam") { bekle(7 / 2).olsun(3) }
  ornek("ondalik") { bekle(7 / 2).olsun(3.5) }
end

p testleri_kos(sinif_testleri)
p testleri_kos(blok_testleri)
{"test_ondalik" => :dustu, "test_tam" => :gecti}
{"bolme tam" => :gecti, "bolme ondalik" => :dustu}

İki tablo aynı sonucu verir: tam sayı bölmesini 3.5 bekleyen test iki biçimde de düştü. Fark yazımdadır. Birinci biçimde test adı yöntem adıdır ve testler bir sınıfta toplanır. İkinci biçimde test adı, konu ile örnek adından kurulan bir dizedir. Koşucu iki durumda da aynı ad → blok tablosunu görür; bu yüzden sonraki bölümlerde ölçülecek sıra ve tohum etkisi koşucuya aittir. RK13a. İki biçim burada tek bir örnekte, iki testle karşılaştırıldı; gerçek çerçevelerin ön hazırlık, iç içe gruplama ve raporlama farkları bu karşılaştırmanın dışındadır.

Gözlemin Koşucusu

Gözlem satırındaki koşucu daha küçüktür ama bir şey daha yapar: testleri tohumla karıştırır ve hepsine aynı ayar tablosunu verir. Aşağıdaki dosya gozlemler.rb’den alınan TESTLER ve test_kos tanımlarını birebir taşır.

# test.rb — gozlem satirinin testleri ve kosucusu
# Test kosucusu: testler tohumla karistirilan sirada kosar ve ayni ayarlari paylasir.
TESTLER = {
  "fiyat_bicimi" => ->(ayar) { "12 #{ayar[:para_birimi]}" == "12 TRY" },
  "doviz_cevirimi" => ->(ayar) { ayar[:para_birimi] = "USD"; true },
  "fatura_toplami" => ->(ayar) { ayar[:para_birimi] == "TRY" }
}

def test_kos(kosum, testler = TESTLER)
  ayar = { para_birimi: "TRY" }
  sira = testler.keys.shuffle(random: Random.new(kosum.tohum))
  sira.reject { |ad| testler[ad].call(ayar) }
end

Üç test var. fiyat_bicimi ayardaki para birimine bakar ve TRY bekler. doviz_cevirimi ayardaki para birimini USD yapar ve doğru döner. fatura_toplami yine TRY bekler. Koşucu düşen testlerin adlarını döndürür. Aşağıdaki dosya altı tohum için karışık sırayı ve düşen testleri yan yana basar.

# tohumlar.rb — alti tohum, alti sira
require_relative "belirleyen"
require_relative "test"

(1..6).each do |tohum|
  sira = TESTLER.keys.shuffle(random: Random.new(tohum))
  puts "#{tohum} #{sira.inspect} #{test_kos(TABAN.with(tohum: tohum)).inspect}"
end
1 ["fiyat_bicimi", "fatura_toplami", "doviz_cevirimi"] []
2 ["fatura_toplami", "doviz_cevirimi", "fiyat_bicimi"] ["fiyat_bicimi"]
3 ["doviz_cevirimi", "fiyat_bicimi", "fatura_toplami"] ["fiyat_bicimi", "fatura_toplami"]
4 ["doviz_cevirimi", "fiyat_bicimi", "fatura_toplami"] ["fiyat_bicimi", "fatura_toplami"]
5 ["fiyat_bicimi", "doviz_cevirimi", "fatura_toplami"] ["fatura_toplami"]
6 ["fiyat_bicimi", "doviz_cevirimi", "fatura_toplami"] ["fatura_toplami"]

Tohum 1 sıralaması doviz_cevirimi‘ni sona koyar; ondan sonra koşan test olmadığı için hiçbir test düşmez. Tohum 2’de doviz_cevirimi ortadadır ve ondan sonra gelen fiyat_bicimi düşer. Tohum 3 ve 4 aynı sırayı verdi: doviz_cevirimi başta, ondan sonra gelen iki test de düştü. Tohum 5 ve 6 de aynı sırayı paylaşır; doviz_cevirimi ortadadır ve yalnız ondan sonra gelen fatura_toplami düşer. Altı tohumdan dört farklı sıra çıktı, ve her sıranın sonucu tohum sabit kaldıkça aynıdır.

Paylaşılan Durum

Kusur sırada değil, sırayı kusur olarak görünür kılan şeydedir. doviz_cevirimi ortak ayar tablosunu değiştirir. Bu değişiklik, ondan sonra koşan her testin girdisidir. Tablonun testlerin arasında paylaşıldığını doğrudan göstermek mümkündür.

# paylasilan.rb — bir testin yazdigi ayar, ondan sonra gelen testin girdisi
require_relative "test"

ayar = { para_birimi: "TRY" }
TESTLER["doviz_cevirimi"].call(ayar)
p ayar
p TESTLER["fiyat_bicimi"].call(ayar)
p TESTLER["fiyat_bicimi"].call({ para_birimi: "TRY" })
{para_birimi: "USD"}
false
true

doviz_cevirimi’nden sonra ayar USD oldu. Aynı fiyat_bicimi testi bu ayarla false döndü, taze bir ayarla true. Test kodu aynıdır; değişen, testin başlangıç durumudur. Bu paylaşılan durumdur ve kararsız testin en yaygın kaynağıdır: bir test iyidir, ama kendisinden önce kimin koştuğuna bağlıdır. RK14. Ölçüm yalnız ayar tablosunu paylaşan bir model için geçerlidir; dosya sistemi, ağ ya da küresel değişken üzerinden paylaşılan durum aynı biçimde davranır, ama burada ölçülmedi.

Hangi Test, Kaç Tohumda Düşüyor

Tek bir tohumun sonucu bir tanıdır; altı tohumun sayımı ise bir profil verir. Aşağıdaki dosya her tohumun düşürdüğü test sayısını, her testin kaç tohumda düştüğünü ve en az bir testin düştüğü tohum sayısını sayar.

# sayim.rb — dusenlerin tohuma gore dagilimi
require_relative "belirleyen"
require_relative "test"

sonuclar = (1..6).map { |t| test_kos(TABAN.with(tohum: t)) }
p sonuclar.map(&:size)
p sonuclar.flatten.tally
p sonuclar.count { |d| !d.empty? }
[0, 1, 2, 2, 1, 1]
{"fiyat_bicimi" => 3, "fatura_toplami" => 4}
5

Altı koşumun beşinde en az bir test düştü; yalnız tohum 1 temiz geçti. doviz_cevirimi hiç düşmedi, ama o, öteki ikisini düşüren testtir. fatura_toplami daha çok tohumda düştü çünkü doviz_cevirimi onun önüne daha sık geçti; fiyat_bicimi daha az düştü. Bu tablo suçluyu göstermez, mağduru gösterir. Düşen test, kusurlu testin kendisi değildir; kusurlu test sessizce geçiyor ve başkasını düşürüyor. Bir testin düşme sıklığı, onun kendi sağlamlığını değil, önüne kimin geçtiğini ölçer. Bu yüzden düşen testin kodu okunurken ilk soru, düşen testin neyi paylaştığı olmalıdır. RK15. Sayım tek bir takımın altı tohumu içindir; düşme sıklığının gerçek bir takımda aynı oranlarda olacağı bu ölçümden çıkmaz.

Sırayı sabitlemek bu sorunu çözmez, saklar. Tohum 1 her koşumda kullanılsaydı bu takım hiç düşmezdi ve paylaşılan durum fark edilmeden kalırdı. Karıştırma bir hata üretmez; var olan bağımlılığı görünür kılar. Ve görünür kılabildiği ölçüde, düşen testin tohumunu basmak, karıştırmayı bir kuşkudan bir tanıya çevirir.

Tohumla Yeniden Üretmek

Kararsız test, düştüğü anda yeniden üretilebilirse bir hata raporudur; üretilemiyorsa bir söylentidir. Koşucu tohumu sabitleyince düşen test her seferinde aynı sırada düşer. Bu yüzden iyi bir koşucu tohumu basar: bir koşum düştüğünde tohum, yeniden üretimin anahtarıdır.

# tekrar.rb — dusen testi tohumla yeniden uret
require_relative "belirleyen"
require_relative "test"

sonuclar = (1..6).map { |t| test_kos(TABAN.with(tohum: t)) }
p sonuclar.count([])
p test_kos(DEGISKENLER[:sira])
p test_kos(DEGISKENLER[:sira]) == test_kos(TABAN.with(tohum: 2))
p belirleyen(->(k) { test_kos(k) })
1
["fiyat_bicimi"]
true
["sira", "B- S+ O-", []]

Altı tohumdan yalnız birinde hiçbir test düşmedi: tohum 1 geçerken tohum 2 düştü. DEGISKENLER[:sira] tohum 2 olduğu için ["fiyat_bicimi"] verdi ve aynı tohumla ikinci çağrı birincinin aynısını verdi. Son satır sınıflandırmayı gösterir: sonuç siradan geliyor, kanıt B- S+ O-, taban []. Kural tabandaki sonucu [] olarak yazdı; düşen test taban koşumunda görünmez, yalnız sira değişkeninde görünür. Bu dersin belirleyeni sıradır. RK16. Tohum 1’in “geçti” göstermesi testin sağlam olduğunu göstermez; yalnız o sırada ayar tablosunu bozan test sona düştüğünü gösterir.

Sınırlayıcı Ölçüm

Sınıf iki yönden sınanır. Birinci yön: her test kendi ayar kopyasıyla koşarsa ne olur? İkinci yön: kural yalnız iki tohumu dener, bu iki tohum her kararsızlığı yakalar mı?

# yalitim.rb — her test kendi ayar kopyasini alir
require_relative "belirleyen"
require_relative "test"

def yalitilmis_test_kos(kosum, testler = TESTLER)
  ayar = { para_birimi: "TRY" }
  sira = testler.keys.shuffle(random: Random.new(kosum.tohum))
  sira.reject { |ad| testler[ad].call(ayar.dup) }
end

p (1..6).map { |t| yalitilmis_test_kos(TABAN.with(tohum: t)) }
p belirleyen(->(k) { yalitilmis_test_kos(k) })
[[], [], [], [], [], []]
["kod", "B- S- O-", []]

Altı tohumun hepsi [] verdi ve kural bu kez kod dedi, kanıt B- S- O-. Aynı üç test, aynı altı tohum; tek fark her testin kendi kopyasını alması. Sınıf sıradan koda geçti: yalıtım sıranın etkisini ortadan kaldırır, çünkü testler artık birbirinin girdisi değil. Yalıtımın sınırı kopyanın derinliğidir.

# yuzeysel.rb — dup yuzeyseldir
ayar = { liste: [] }
kopya = ayar.dup
kopya[:liste] << 1
p ayar
p ayar[:liste].equal?(kopya[:liste])
{liste: [1]}
true

dup tabloyu kopyaladı, içindeki diziyi kopyalamadı: iki tablo aynı diziyi tutuyor ve kopyadaki değişiklik aslına geçti. Ayar tablosu yalnız basit değerler taşıdığı için bu dersin yalıtımı çalıştı. İç içe bir ayar için dup yetmez. RK17. Yalıtım ölçümü yalnız düz bir tablo için geçerlidir.

İkinci yön, kuralın kendi kör noktasıdır. Kural tabanı ve sira değişkenini, yani tohum 1 ve tohum 2’yi dener. Tohum 2’nin geçtiği ama tohum 5’in düştüğü bir test takımı kurulabilir. fiyat_bicimi testini ayara hiç bakmayan bir testle değiştirmek yeter.

# kor.rb — kuralin yalniz tohum 1 ve 2'yi denedigi yer
require_relative "belirleyen"
require_relative "test"

TESTLER_KOR = TESTLER.merge("fiyat_bicimi" => ->(_ayar) { true })

p belirleyen(->(k) { test_kos(k, TESTLER_KOR) })
sonuclar = (1..6).map { |t| test_kos(TABAN.with(tohum: t), TESTLER_KOR) }
p sonuclar
["kod", "B- S- O-", []]
[[], [], ["fatura_toplami"], ["fatura_toplami"], ["fatura_toplami"], ["fatura_toplami"]]

Kural bu takımı kod olarak sınıflandırdı, çünkü tohum 1 ve 2 ikisi de [] verdi. Ama altı tohumdan dördünde fatura_toplami düştü. “Sıra değil” sonucu yalnız denenen iki tohum için söylenmiştir. Takım hâlâ paylaşılan duruma bağlı; kural onu görmedi çünkü bakmadığı sıralarda düşüyor. Bunun çaresi kuralı değiştirmek değil, ölçümü yorumlamaktır: kuralın kod dediği bir test takımı, tohum aralığı genişletilmeden yalıtılmış sayılmaz. Yukarıdaki iki ölçüm birlikte okunmalıdır. Yalıtılmış koşucu altı tohumun hepsinde [] verdi; sınanan takım ise yalnız tohum 1 ve 2’de [] verdi. Birincide [] altı tohumun tümünde görüldü, ikincisinde yalnız ikisinde. Aynı çıktı, farklı sayıda tohumla desteklenir, ve ölçümün değeri o sayıdan gelir. RK18. Altı tohum altı olası sıranın yalnız dördünü verdi; testlerin sayısı arttıkça olası sıra sayısı hızla büyür ve birkaç tohum onları tüketemez.

Özet

  • Bir test çerçevesi üç parçadır: test tanımı, iddia ve koşucu. Düşen test iddia hatasıdır, patlayan test başka bir hatadır.
  • İki yazım biçimi, yani test_ yöntemli test sınıfı ve beklenti zincirli tanım bloğu, koşucuya aynı ad → blok tablosunu verir; aynı iki test iki biçimde de aynı sonucu verdi.
  • Bu dersin belirleyeni sıradır: test sonucu satırında kanıt B- S+ O-, tabanda [], tohum 2’de ["fiyat_bicimi"].
  • Kararsız testin kaynağı paylaşılan durumdur: doviz_cevirimi ortak ayarı değiştirir ve kendisinden sonra koşan testi düşürür. Tohum basılırsa düşen test yeniden üretilir.
  • Her test kendi ayar kopyasını alınca altı tohumun hepsi [] verir ve kural kod der; dup yüzeyseldir.
  • Kural yalnız tohum 1 ve 2’yi dener; tohum 2’nin geçtiği ama tohum 5’in düştüğü bir takımı kod diye sınıflandırır.

Sonraki Adım

Sırayı tohum seçiyor ve yalıtım onu kaldırıyor. Ama bir test başka bir girdiye de bağlı olabilir: bugünün saati. Bir sonraki ders saati okuyan bir testin neden yarın düşeceğini, ve sahteleme ile test verisinin bu bağımlılığı nasıl kaldırdığını ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat