Ders 12 / 13
Test
Bir denetleyicinin örnek değişkeninin dışarıdan okunmasının, birim testi ile özellik testi arasındaki farkın ve her testin kendi uygulamasını kurmasının yalıtım üzerindeki etkisinin ölçümü.
Yayın Güncelleme
Bu derste
- Bir denetleyicinin örnek değişkenini dışarıdan okuyarak birim testi yazar
- Birim testi ile özellik testini kapsamlarına göre ayırt eder
- Saf Ruby ile küçük bir doğrulama yardımcısı kurar ve bir başarısızlığın nasıl bildirildiğini gösterir
- Her testin kendi uygulamasını kurmasının yalıtımı nasıl sağladığını, paylaşılan bir uygulamanın neyi bozduğunu ölçer
İçindekiler
Önceki ders, bir parçanın kalıcı kalışının avantajını ve bayatlama bedelini ölçtü. Kalıcı bir değere güvenmek için o değeri üreten kodun doğru çalıştığından emin olmak gerekir; bunu kontrol etmenin yolu testtir. Bir test de bir değerin kalışına dayanır: testin kurduğu durum, testin sonunda nereye kadar ulaşıyor? Bir testin bıraktığı kayıt bir sonrakinin başlangıç durumuna karışıyorsa testler birbirinden bağımsız değildir ve sonuçları sırasına göre değişir.
Bu derste ölçü ekseni aynı kalır: bu değer hangi isteğe kadar kalıyor? Dört kalış: bu istek, sonraki istek, oturum, kalıcı. Bir denetleyicinin örnek değişkeni bir isteğe aittir; bir uygulamanın deposu ise bütün isteklere. Testin yalıtımı, bu iki kalışın testler arasında karışıp karışmadığına bağlıdır.
Çekirdek
Ölçümler aynı küçük istek döngüsüne dayanır. Dosya kursun çekirdeğidir ve olduğu gibi verilir.
# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci
# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
def initialize(gelen)
@gelen = gelen
@yeni = {}
end
def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }
def []=(anahtar, deger)
@yeni[anahtar] = deger
end
def birakilan = @yeni
end
# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu
def initialize(uygulama, oturum)
@uygulama = uygulama
@params = {}
@oturum = oturum
@flash = Flash.new(oturum.delete(:flash) || {})
@istek_deposu = {}
end
end
# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
attr_reader :depo
def initialize
@oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
@depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
kuyruk: [], onbellek: {}, sayac: 0 }
end
def istek(istemci)
baglam = Baglam.new(self, @oturumlar[istemci])
sonuc = yield baglam
baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
sonuc
end
end
# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
uygulama = Uygulama.new
ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?
gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
end
tur = if gorulen[2] != "--" then "kalici"
elsif gorulen[1] != "--" then "oturum"
elsif gorulen[0] != "--" then "sonraki"
else "bu_istek"
end
[tur, ["A1", *gorulen].join(" "), ilk]
end
Bu blok kaynaktır; dosya diske yazılır ve require_relative ile çekilir. Çekirdek bir çerçeve değil, bir çerçevenin modelidir. Bir fark taşır: çekirdekteki Urun.bul, saklanan nesnenin kendisini döndürür; birçok eşleyicide her okuma, satırdan yeni bir nesne kurar. Bu dersteki ölçümler bu farkın gözlemlenebilir sonuçlarına dayanmaz.
Denetleyiciyi Dışarıdan Okumak
Bir denetleyici eylemi çalıştırdığında ne yaptığını iki yerde bırakır: döndürdüğü değerde ve örnek değişkenlerinde. İkincisi görünüme taşınan durumdur ve bir birim testi (unit test) onu doğrudan okuyabilir. Birim testi, tek bir parçayı, burada tek bir denetleyiciyi, öteki parçalardan ayırarak sınar. Ruby’de örnek değişkeni dışarıdan instance_variable_get ile okunur. Kesit, önceki derslerden tanıdık Urun ve UrunlerDenetleyicisi ile denetleyici yardımcısını tek dosyada toplar.
# test.rb — Urun, UrunlerDenetleyicisi ve denetleyici yardimcisi
require_relative "kalis"
module Urun
def self.olustur(b, nitelikler)
hatalar = []
hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
return { hatalar: } unless hatalar.empty?
tablo = b.uygulama.depo[:urunler]
id = tablo.size + 1
tablo[id] = nitelikler.merge(id:)
{ id:, hatalar: }
end
def self.bul(b, id)
sorgular = (b.istek_deposu[:sorgular] ||= {})
sorgular.fetch([:urunler, id]) do
b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
end
end
end
class UrunlerDenetleyicisi
def self.once(*adlar) = (@filtreler ||= []).concat(adlar)
def self.filtreler = @filtreler || []
once :urunu_bul
def initialize(baglam)
@baglam = baglam
end
def calistir(eylem)
self.class.filtreler.each { |filtre| send(filtre) }
public_send(eylem)
end
def goster = @urun
private
def urunu_bul
@urun = Urun.bul(@baglam, @baglam.params[:id])
end
end
def denetleyici(b) = b.istek_deposu[:denetleyici] ||= UrunlerDenetleyicisi.new(b)
def goster(ad, tur, kanit, deger)
printf("%-14s%-14s%-13s%s\n", ad, tur, kanit, deger.inspect)
end
Bu blok da kaynaktır. İlk ölçüm, eylemden sonra denetleyicinin örnek değişkenini okur ve kalışını sınıflandırır.
# degisken.rb — denetleyici degiskeni hangi istege kadar kaliyor
require_relative "test"
kalem = ->(b) { Urun.olustur(b, ad: "kalem", fiyat: 12) }
yaz = ->(b) { kalem.call(b); b.params[:id] = 1; denetleyici(b).calistir(:goster) }
oku = ->(b) { denetleyici(b).instance_variable_get(:@urun)&.fetch(:ad) }
goster("denetleyici", *kalis(yaz, oku))
denetleyici bu_istek A1 -- -- -- "kalem"
Örnek değişken yalnız eylemin çalıştığı istekte görüldü. Sonraki istekte denetleyici(b) yeni bir denetleyici kurar ve onun @urun değeri boştur. Bu bu istek kalışıdır: denetleyici, istek deposundan kurulur ve istekle birlikte gider. Bu aynı zamanda bir test için bir kolaylıktır, çünkü bir testin okuduğu durum önceki testten gelmiş olamaz. Kayıt ise farklıdır: Urun.olustur uygulamanın deposuna yazar, o depo isteklerden uzun yaşar. İki kalışı birbirinden ayırmak yalıtımın ilk koşuludur. UY9. Burada denetleyiciyi doğrudan kurup calistir ile çağırdık; bir yönlendirme ya da bir ara katman devreye girmedi, yani ölçüm bir birim testidir.
Küçük Bir Doğrulama Yardımcısı
Bir test aracına ihtiyaç duymadan test yazmak mümkündür: beklenen ve gelen değeri karşılaştırıp uyuşmazlıkta bir istisna fırlatan yeterlidir. Ders bir test aracı tanıtmaz; yalnız bir doğrulamanın en küçük biçimini gösterir ve başarısız olduğunda ne olduğunu ölçer.
# dogrula.rb — en kucuk dogrulama yardimcisi
require_relative "test"
class DogrulamaHatasi < StandardError; end
def dogrula(beklenen, gelen)
raise DogrulamaHatasi, "beklenen #{beklenen.inspect}, gelen #{gelen.inspect}" unless beklenen == gelen
:gecti
end
Bu blok da kaynaktır. Yardımcıyı bir ürün oluşturan istekte iki kez çağıralım; ikincisinde beklenen değer yanlış verilir.
# basarisiz.rb — gecen ve basarisiz olan dogrulama
require_relative "dogrula"
uygulama = Uygulama.new
sonuc = uygulama.istek("A") do |b|
Urun.olustur(b, ad: "kalem", fiyat: 12)
b.params[:id] = 1
denetleyici(b).calistir(:goster)
end
p dogrula("kalem", sonuc[:ad])
begin
dogrula("silgi", sonuc[:ad])
rescue DogrulamaHatasi => e
puts "#{e.class}: #{e.message}"
end
:gecti DogrulamaHatasi: beklenen "silgi", gelen "kalem"
İlk doğrulama geçti. İkincisi, beklenen değer gelen değerden farklı olduğu için, bir istisna fırlattı ve istisnanın iletisi iki değeri de yazdı. Bir test aracının yaptığı temelde budur: farkı bir hata olarak bildirmek ve testin geri kalanını bu hatayla durdurmak. Gerisi, yani testleri bulmak, sırayla çalıştırmak ve sonuçları özetlemek, bu çekirdeğin üzerine eklenir.
Birim Testi ve Özellik Testi
Birim testi tek bir parçayı sınar. Özellik testi (feature test) ise bir kullanıcının izleyeceği yolu baştan sona sınar: istek gelir, işlenir, yanıt döner. İkisi arasındaki fark kapsamdır, ve kapsam arttıkça bir başarısızlığın nedenini bulmak zorlaşır. Aynı davranışı iki kapsamda ölçelim: ürün oluşturulur ve bulunur. Birimde Urun ile denetleyici doğrudan çağrılır. Özellikte, bir isteğin içinden geçen yolu taşıyan tek bir çağrı vardır.
# kapsam.rb — birim ve ozellik testi ayni davranisi farkli kapsamda sinar
require_relative "dogrula"
# birim: yalniz Urun
uygulama = Uygulama.new
uygulama.istek("A") do |b|
Urun.olustur(b, ad: "kalem", fiyat: 12)
p dogrula("kalem", Urun.bul(b, 1)[:ad])
end
# ozellik: bir istegin icinden gecen yol, denetleyici dahil
uygulama = Uygulama.new
uygulama.istek("A") { |b| Urun.olustur(b, ad: "kalem", fiyat: 12) }
yanit = uygulama.istek("B") do |b|
b.params[:id] = 1
denetleyici(b).calistir(:goster)
end
p dogrula("kalem", yanit[:ad])
:gecti :gecti
İki test de geçti, ama biri Urun’u yalnız çağırdı; ötekisi başka bir istemcinin isteğine kadar yolu izledi. Özellik testi, bir parçanın başkasıyla birleşimini sınadığı için daha çok şeyi doğrular ve bozulduğunda bozulan şeyi göstermez. Birim testi daha az şeyi doğrular ve bozulduğunda yeri açıktır. İkisi birbirinin yerini tutmaz.
Başarısız Test Yazdığını Geri Almaz
Bir doğrulama başarısız olduğunda istisna testin geri kalanını durdurur. Fakat testin o ana kadar yaptığı yazma durur mu, geri alınır mı? Bunu uygulamayı paylaşan bir testte ölçelim: test önce bir ürün oluşturur, sonra yanlış bir beklentiyle başarısız olur. Ardından uygulamanın deposuna bakılır.
# kalinti.rb — basarisiz test yazdigi urunu geri alir mi
require_relative "dogrula"
paylasilan = Uygulama.new
begin
paylasilan.istek("A") do |b|
Urun.olustur(b, ad: "kalem", fiyat: 12)
dogrula("silgi", Urun.bul(b, 1)[:ad])
end
rescue DogrulamaHatasi => e
puts e.class
end
p paylasilan.depo[:urunler].size
DogrulamaHatasi 1
Doğrulama istisna fırlattı, ama ürün depoda kaldı. İstisna, testin akışını keser; uygulamanın deposuna yapılmış bir yazmayı silmez. Bu, bir hatanın iki sonucu olduğu anlamına gelir: testin kendisi başarısız olur ve onun bıraktığı kalıntı, deponun kalıcı kalışı nedeniyle sonraki testlerin başlangıç durumuna girer. Başarısız bir testin ardından gelen testlerin de bozulabilmesinin nedenlerinden biri budur. Kalıntıyı temizlemeyi testin kendisine bırakmak çözüm sayılmaz, çünkü temizleme kodu da başarısız testin kestiği akışın arkasında kalabilir; daha güvenilir olan, her teste kendi uygulamasını yeniden kurmaktır. Bu çekirdekte kurmanın maliyeti bir Uygulama.new çağrısıdır; maliyet bu kadar düşük olduğunda paylaşmanın kazancı bu çekirdekte yoktur.
Sıra Sonucu Değiştirirse
Yalıtımın bozulduğunu aşağıdaki sınırlayıcı ölçüm iki kimlikle gösterecek; burada aynı bozulmayı bir testin geçip geçmediği üzerinden ölçelim. İki test aynı uygulamayı paylaşır: biri ilk ürünün kimliğinin 1 olmasını bekler, öteki yalnız bir ürün oluşturur. Testler iki farklı sırayla koşturulur ve her koşuda yeni bir uygulama kurulur.
# sira_testi.rb — ayni iki test, iki sirada, ortak uygulamada
require_relative "dogrula"
bekleyen = ->(b) { dogrula(1, Urun.olustur(b, ad: "kalem", fiyat: 12)[:id]) }
yazan = ->(b) { Urun.olustur(b, ad: "silgi", fiyat: 5) }
{ "bekleyen, yazan" => [bekleyen, yazan], "yazan, bekleyen" => [yazan, bekleyen] }.each do |ad, siralama|
uygulama = Uygulama.new
sonuclar = siralama.map do |test|
uygulama.istek("A") { |b| test.call(b) }
:gecti
rescue DogrulamaHatasi
:basarisiz
end
puts "#{ad}: #{sonuclar.inspect}"
end
bekleyen, yazan: [:gecti, :gecti] yazan, bekleyen: [:gecti, :basarisiz]
Aynı iki test, bir sırada geçti, ötekinde başarısız oldu. Testlerin her biri tek başına doğru görünür; sorun, ikisinin uygulamayı paylaşmasındadır. Sıraya bağlı bir başarısızlık, testin bir davranışı değil, testlerin birbirine bıraktığı durumu sınadığının işaretidir.
Önbelleği Paylaşan Testler
Yalıtımın önbellek için de geçerli olduğunu bir ölçümle gösterelim. İki test aynı uygulamayı paylaşır. İlk test uygulamanın önbelleğine bir değer yazar. İkinci test ise önbelleğin boş olduğunu varsayarak aynı anahtarı kendi değeriyle doldurmak ister.
# onbellek_sizinti.rb — onbellegi paylasan ikinci test kendi degerini gorur mu
require_relative "dogrula"
paylasilan = Uygulama.new
paylasilan.istek("A") { |b| b.uygulama.depo[:onbellek]["k"] ||= "ilk testin degeri" }
okunan = paylasilan.istek("A") { |b| b.uygulama.depo[:onbellek]["k"] ||= "ikinci testin degeri" }
p okunan
yalitilmis = Uygulama.new.istek("A") { |b| b.uygulama.depo[:onbellek]["k"] ||= "ikinci testin degeri" }
p yalitilmis
"ilk testin degeri" "ikinci testin degeri"
Ortak uygulamada ikinci test, kendi değerini değil birincinin değerini okudu; yalıtılmış uygulamada ise kendi değerini. Önbellek, kalıcı kalışı nedeniyle testler arasında bir sızıntı kanalıdır ve bu sızıntı testin başarısız olduğunu söylemez: ikinci test geçebilir, ama beklediği şeyi sınamamıştır.
Üretim Ortamıyla İlişki
M12/K05’te test ortamının üretime çıkıp çıkmadığı ölçülmüştü: bir ortamın yapılandırması bir sürecin parçasıdır ve bir sürecin dışına çıkmaz. Burada aynı ilke bir adım ötede geçerlidir. Test, kendi uygulamasını kurar; o uygulamanın deposu ve oturumları yalnız o testin sürecinde, o testin kurduğu nesnelerin içinde yaşar. Bu bir test için güvenlik sağlar: bir test, üretim uygulamasının kaydını okumaz ya da değiştirmez.
Sınırlayıcı Ölçüm
Yalıtımın bozulduğu durum, iki testin aynı uygulamayı paylaştığı durumdur. İlk ölçüm, her testin kendi uygulamasını kurduğu durumdur: iki ayrı Uygulama.new içinde ilk ürünün kimliği ne?
# yalitim.rb — her test kendi uygulamasini kurarsa
require_relative "test"
ilk = Uygulama.new.istek("A") { |b| Urun.olustur(b, ad: "kalem", fiyat: 12)[:id] }
ikinci = Uygulama.new.istek("A") { |b| Urun.olustur(b, ad: "silgi", fiyat: 5)[:id] }
puts "ayri uygulamalar: #{ilk} ve #{ikinci}"
ayri uygulamalar: 1 ve 1
İki test de ilk kayıt olarak 1 kimliğini aldı: biri öteki olmadan çalışabilir ve sırası sonucu etkilemez. İkinci ölçüm, aynı uygulamayı paylaşan iki testi kurar.
# paylasim.rb — iki test ayni uygulamayi paylasirsa
require_relative "test"
paylasilan = Uygulama.new
ilk = paylasilan.istek("A") { |b| Urun.olustur(b, ad: "kalem", fiyat: 12)[:id] }
ikinci = paylasilan.istek("A") { |b| Urun.olustur(b, ad: "silgi", fiyat: 5)[:id] }
puts "ortak uygulama: #{ilk} ve #{ikinci}"
ortak uygulama: 1 ve 2
İkinci test, bağımsız çalıştığında 1 alacakken, ilk testten sonra 2 aldı. Bir test 1 bekliyorsa, ortak uygulamada yalnız ilk sırada çalıştığında geçer; başka bir sırada ya da tek başına çalıştırıldığında sonucu değişir. Buna testler arası sızıntı denebilir: uygulamanın deposu kalıcı kalışta olduğu için, testin yazdığı değer sonraki testin okuduğu durumu değiştirir. Çözüm yalıtımdır: her test kendi uygulamasını, o uygulamanın deposunu ve istemcilerini baştan kurar. Bu, önceki dersin parça önbelleği için de geçerlidir. Test bir önbelleği paylaşırsa bir testin yazdığı parça sonraki testin gördüğü parça olur ve bayat içerik ile aynı biçimde testin sonucunu değiştirir.
Özet
- Denetleyicinin örnek değişkeni bu istek kalışındadır; sonraki istekte yeni bir denetleyici kurulur ve değişken boştur.
- Bir birim testi, örnek değişkeni dışarıdan okuyarak tek bir parçayı sınar; en küçük doğrulama yardımcısı, beklenen ile gelen farklıysa bir istisna fırlatır.
- Özellik testi bir yolu baştan sona sınar; birim testi daha az doğrular ama bozulduğunda yeri gösterir.
- Her testin kendi uygulamasını kurması yalıtımı sağlar: ayrı uygulamalarda ilk kimlik
1ve1, paylaşılan uygulamada1ve2. - Uygulamanın deposu kalıcı kalışta olduğu için paylaşılan bir uygulama testler arası sızıntıya yol açar: başarısız test kalıntı bırakır, sonuç sıraya bağlı olur.
- Paylaşılan önbellek de sızıntı kanalıdır; ikinci test geçse bile beklediği şeyi sınamamış olabilir.
Sonraki Adım
Bir uygulamada doğrulanması gereken son şey, uygulamanın ortak güvenlik varsayılanlarıdır. Bir sonraki ders sahtecilik belirtecinin neden oturumda tutulduğunu sorar ve bu belirtecin kalışını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.