Ders 02 / 16
Tablo Güdümlü Testler
Beş vakalık bir tablonun vaka sayısı kaynaktan sayılıyor, koşan alt test sayısı ise yalnız go test -v ile koşturularak okunuyor ve ikisi 5'te eşleşiyor. Sınırlayıcı ölçüm, tabloya aynı satırları koşturan altıncı bir vaka eklendiğinde kapsama oranının 100.0% olarak değişmeden kaldığını gösteriyor.
Yayın Güncelleme
Bu derste
- Bir tablo güdümlü test dosyasındaki vaka sayısını kaynaktan sayar, koşan alt test sayısını koşumdan okur ve ikisini karşılaştırır
- t.Run ile açılan alt testlerin adlandırılmasının ve raporlanmasının nasıl çalıştığını koşturarak gösterir
- Vaka eklemenin kapsama oranını her zaman artırmadığını, aynı satırı koşturan iki vakayla ölçer
İçindekiler
Önceki ders bir testin verdiği kararın kaynaktan değil koşumdan geldiğini ölçtü; kaç test
işlevi yazıldığı kaynaktan sayılabiliyordu, ama hangisinin geçtiği yalnız go test
çalıştırılarak öğrenilebiliyordu. Bu ders aynı ayrımı bir düzey daha ayrıntılı bir yere
taşıyor: tek bir test işlevinin içinde, birden çok vakanın nasıl sınandığına. Vaka
listesinin kendisi bu kez kaynakta yazılı — konunun iki “kaynak” satırından biri — ama
o listenin koştuğunda kaç ayrı alt test doğurduğu yine yalnız koşumdan okunabiliyor.
Bu kalıba tablo güdümlü test deniyor ve Go’ya özgü değil; aynı kalıp başka dillerin test
kütüphanelerinde parametreli test adıyla geçiyor; burada taksonomi tekrar anlatılmıyor. Bu dersin
eklediği soru dar: test paketinin kendi t.Run yordamı bir tabloyu alt testlere nasıl bölüyor ve
vaka sayısıyla koşan alt test sayısı arasındaki ilişki gerçekten birebir mi. Bu soruyu yanıtlamak,
kaynaktan sayılabilen bir bilgiyle koşumdan okunan bir bilgiyi aynı ölçümde yan yana koymayı
gerektiriyor.
Üretim kod tabanlarında tek bir işlevi onlarca girdiyle sınamak sık görülen bir ihtiyaç
ve bu ihtiyacı her girdi için ayrı bir func Test... yazarak karşılamak hem tekrarlı
hem de okunması zor bir dosya doğuruyor. Tablo güdümlü biçim bu tekrarı bir kez yazılan
bir gövdeye indirgiyor; asıl soru burada, o tek gövdenin arkasında duran vaka sayısının
kaynaktan mı yoksa yalnız koşumdan mı okunabildiği.
envanter paketi bu derste de aynı yol dizesi altında kalıyor; yalnız içine bir işlev
daha ekleniyor. Önceki dersteki ToplamDeger bir sayı üretiyordu, Gecerli ise bir
doğruluk değeri üretiyor — ikisi de girdi olarak aynı Kalem tipini alıyor ve bu
süreklilik konunun altı dersi boyunca aynı paketin farklı köşelerinin sınandığını
gösteriyor.
Vaka Listesi Kaynakta Yazılı
envanter paketine bir işlev daha ekleniyor: Gecerli, bir kalemin adının boş
olmadığını, adedinin ve fiyatının pozitif olduğunu sırayla denetliyor ve ilk başarısız
denetimde false döndürüyor. Bu işlevi sınayan tablo, her satırı bir vakaya karşılık
gelen bir dilim değişkeni olarak yazılıyor.
Gecerlinin üç denetimi de aynı biçimde yazıldı: bir koşul, bir erken dönüş. Bu yapı tabloyu
tasarlamayı kolaylaştırıyor, çünkü her denetim için ayrı bir vaka yazmak yeterli — bir vakanın
hangi denetimi tetiklediğini görmek için işlevin gövdesine bakmak gerekmiyor; vakanın kendi adı
("bos ad", "sifir adet", "negatif fiyat", "negatif adet") hangi denetimi hedeflediğini
zaten söylüyor. Vaka adının kendisi de bir kaynak bilgisi: hangi dalın sınandığını, o dalı yazan
kişi tabloya yazarken bildiriyor.
TS4. Aşağıdaki test dosyasındaki vakalar dilimi beş satır içeriyor; bu sayı hiçbir
şey çalıştırmadan, yalnız kaynaktaki {" ile başlayan satırları sayarak okunabiliyor —
tabloyu kim yazdıysa, kaç vaka eklediği de aynı kişinin elinde, kaynağın kendisinde.
Önceki dersteki araclar.Kur ve araclar.Cagir yordamları burada da aynen kullanılıyor;
tanımları değişmedi.
// araclar/araclar.go — go komut ailesini gecici bir modulde program icinden cagiran ortak yordamlar
package araclar
import (
"os"
"os/exec"
"path/filepath"
)
func Kur(dosyalar map[string]string) string {
dizin, _ := os.MkdirTemp("", "sinama")
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
for ad, icerik := range dosyalar {
os.WriteFile(filepath.Join(dizin, ad), []byte(icerik), 0o644)
}
return dizin
}
func Cagir(dizin string, ekOrtam []string, arg ...string) (string, bool) {
cmd := exec.Command("go", arg...)
cmd.Dir = dizin
cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local"), ekOrtam...)
cikti, err := cmd.CombinedOutput()
return string(cikti), err == nil
}
// olcum.go — vaka sayisi kaynaktan sayiliyor, kosan alt test sayisi kosumdan okunuyor
package main
import (
"fmt"
"strings"
"ders/araclar"
)
const envanterMetni = `package sinama
type Kalem struct {
Ad string
Adet int
Fiyat int
}
func Gecerli(k Kalem) bool {
if k.Ad == "" {
return false
}
if k.Adet <= 0 {
return false
}
if k.Fiyat <= 0 {
return false
}
return true
}
`
const testMetniBes = `package sinama
import "testing"
var vakalar = []struct {
ad string
kalem Kalem
istenen bool
}{
{"gecerli", Kalem{"kalem", 3, 250}, true},
{"bos ad", Kalem{"", 3, 250}, false},
{"sifir adet", Kalem{"kalem", 0, 250}, false},
{"negatif fiyat", Kalem{"kalem", 3, -10}, false},
{"negatif adet", Kalem{"kalem", -1, 250}, false},
}
func TestGecerli(t *testing.T) {
for _, v := range vakalar {
t.Run(v.ad, func(t *testing.T) {
if got := Gecerli(v.kalem); got != v.istenen {
t.Errorf("Gecerli(%v) = %v, istenen %v", v.kalem, got, v.istenen)
}
})
}
}
`
func vakaSayisi(kod string) int {
return strings.Count(kod, "\n\t{\"")
}
func main() {
fmt.Println("-- kaynak: vaka sayisi --")
fmt.Println("bes vakali tabloda:", vakaSayisi(testMetniBes))
fmt.Println()
fmt.Println("-- kosum: kosan alt test sayisi --")
dizin := araclar.Kur(map[string]string{"envanter.go": envanterMetni, "envanter_test.go": testMetniBes})
cikti, _ := araclar.Cagir(dizin, nil, "test", "-v", "./...")
fmt.Println("kosan alt test sayisi:", strings.Count(cikti, "--- PASS: TestGecerli/"))
}
-- kaynak: vaka sayisi -- bes vakali tabloda: 5 -- kosum: kosan alt test sayisi -- kosan alt test sayisi: 5
TS5. Kaynaktan sayılan vaka sayısı ile koşumdan okunan alt test sayısı bu ölçümde
eşleşiyor: 5. Bu eşleşme rastlantı değil, t.Run’ın kendi kuralının sonucu — döngü
vakalar dilimini gezerken her turda t.Run(v.ad, ...) çağrısı bir alt test açıyor ve
test paketi bu alt testi çağırana verdiği adla (v.ad) raporluyor: çıktıda
TestGecerli/gecerli, TestGecerli/bos_ad gibi satırlar beliriyor. strings.Count
çağrısı bu satırların kaç tanesinin PASS ile bittiğini sayıyor ve tablo doğru
yazıldığı sürece bu sayı her zaman vakaların uzunluğuna eşit çıkıyor — vaka sayısı ile
alt test sayısı arasındaki bağ, döngünün kendisinde kurulu.
Bu eşleşme, tek bir TestGecerli işlevinin aslında beş ayrı sınama yürüttüğünü ama
kaynakta beş ayrı işlev olarak görünmediğini gösteriyor. Önceki derste kaynaktan
sayılan şey func Test geçişleriydi ve test işlevi sayısını veriyordu; burada sayılan
şey aynı işlevin içindeki veri satırları ve verdiği şey alt test sayısı. İkisi de
kaynaktan okunabilen bir bilgi, ama iki ayrı düzeyde: bir dosyada kaç test işlevi var
ve bir test işlevi kaç alt test açıyor.
Alt test adları da kendi başına küçük bir dönüşümden geçiyor: v.ad içindeki boşluk
karakterleri test paketi tarafından alt çizgiye çevriliyor, bu yüzden "bos ad" vakası
çıktıda TestGecerli/bos_ad olarak görünüyor. Bu dönüşüm ölçümü etkilemiyor çünkü
strings.Count yalnız --- PASS: TestGecerli/ ön ekini arıyor, tam adı değil — ama
kaynaktaki bir vaka adını okuyup çıktıda birebir aynı dizgiyi aramak isteyen biri bu
dönüşümü bilmek zorunda, çünkü kaynaktaki "bos ad" ile koşumdaki bos_ad aynı vakaya
işaret etse de aynı bayt dizisi değil.
Sınırlayıcı Ölçüm: Vaka Eklemek Kapsamayı Artırmayabilir
Tabloya bir vaka eklemek sezgisel olarak “daha fazla sınamak” gibi görünüyor. Aşağıdaki
ölçüm bunun her zaman doğru olmadığını gösteriyor: beş vakalık tabloya, Gecerlinin
zaten sınadığı bir satırı yeniden koşturan altıncı bir vaka ekleniyor — ad yine boş, ama
adet ve fiyat farklı — ve kapsama oranı iki tabloda da ölçülüyor.
// kapsam.go — ayni satiri kosturan yeni bir vaka kapsama oranini degistirmiyor
package main
import (
"fmt"
"strings"
"ders/araclar"
)
const envanterMetni = `package sinama
type Kalem struct {
Ad string
Adet int
Fiyat int
}
func Gecerli(k Kalem) bool {
if k.Ad == "" {
return false
}
if k.Adet <= 0 {
return false
}
if k.Fiyat <= 0 {
return false
}
return true
}
`
const testMetniBes = `package sinama
import "testing"
var vakalar = []struct {
ad string
kalem Kalem
istenen bool
}{
{"gecerli", Kalem{"kalem", 3, 250}, true},
{"bos ad", Kalem{"", 3, 250}, false},
{"sifir adet", Kalem{"kalem", 0, 250}, false},
{"negatif fiyat", Kalem{"kalem", 3, -10}, false},
{"negatif adet", Kalem{"kalem", -1, 250}, false},
}
func TestGecerli(t *testing.T) {
for _, v := range vakalar {
t.Run(v.ad, func(t *testing.T) {
if got := Gecerli(v.kalem); got != v.istenen {
t.Errorf("Gecerli(%v) = %v, istenen %v", v.kalem, got, v.istenen)
}
})
}
}
`
const testMetniAlti = `package sinama
import "testing"
var vakalar = []struct {
ad string
kalem Kalem
istenen bool
}{
{"gecerli", Kalem{"kalem", 3, 250}, true},
{"bos ad", Kalem{"", 3, 250}, false},
{"sifir adet", Kalem{"kalem", 0, 250}, false},
{"negatif fiyat", Kalem{"kalem", 3, -10}, false},
{"negatif adet", Kalem{"kalem", -1, 250}, false},
{"bos ad, farkli miktar", Kalem{"", 9, 500}, false},
}
func TestGecerli(t *testing.T) {
for _, v := range vakalar {
t.Run(v.ad, func(t *testing.T) {
if got := Gecerli(v.kalem); got != v.istenen {
t.Errorf("Gecerli(%v) = %v, istenen %v", v.kalem, got, v.istenen)
}
})
}
}
`
func vakaSayisi(kod string) int {
return strings.Count(kod, "\n\t{\"")
}
func kapsamOrani(cikti string) string {
for _, s := range strings.Split(cikti, "\n") {
if i := strings.Index(s, "coverage: "); i >= 0 {
return strings.TrimSpace(s[i+len("coverage: "):])
}
}
return "yok"
}
func main() {
dizinBes := araclar.Kur(map[string]string{"envanter.go": envanterMetni, "envanter_test.go": testMetniBes})
ciktiBes, _ := araclar.Cagir(dizinBes, nil, "test", "-cover", "./...")
fmt.Println("bes vakali tabloda vaka sayisi:", vakaSayisi(testMetniBes))
fmt.Println("bes vakali tabloda kapsama orani:", kapsamOrani(ciktiBes))
dizinAlti := araclar.Kur(map[string]string{"envanter.go": envanterMetni, "envanter_test.go": testMetniAlti})
ciktiAlti, _ := araclar.Cagir(dizinAlti, nil, "test", "-cover", "./...")
fmt.Println("alti vakali tabloda vaka sayisi:", vakaSayisi(testMetniAlti))
fmt.Println("alti vakali tabloda kapsama orani:", kapsamOrani(ciktiAlti))
}
bes vakali tabloda vaka sayisi: 5 bes vakali tabloda kapsama orani: 100.0% of statements alti vakali tabloda vaka sayisi: 6 alti vakali tabloda kapsama orani: 100.0% of statements
TS6. Vaka sayısı 5’ten 6’ya çıkıyor, ama kapsama oranı ikisinde de aynı: 100.0% of statements. Bunun nedeni kapsama aracının satır düzeyinde ölçüm yapması: altıncı vaka,
Gecerlinin ilk if k.Ad == "" { return false } satırını yeniden koşturuyor — ikinci vaka ("bos ad") zaten bu satırı bir kez koşturmuştu ve aracın gözünde bir satırın iki kez mi yoksa bir kez
mi koştuğu fark etmiyor, yalnız koşup koşmadığı önemli. Yeni vaka farklı bir Adet ve Fiyat
taşısa bile, Gecerli bu değerlere hiç bakmadan ilk denetimde dönüyor; kapsama aracı bu farkı
göremiyor çünkü onun ölçtüğü şey hangi satırların çalıştığı, hangi değerlerin denendiği
değil.
Beş vakalık tabloda hangi vakanın hangi satırı koşturduğu tek tek izlenebilir:
"gecerli" üç denetimi de geçip son return true satırına ulaşıyor, "bos ad" ilk
return false satırında duruyor, "sifir adet" ikinci return false’a,
"negatif fiyat" üçüncü return false’a kadar ilerleyip orada duruyor ve "negatif adet" yine
ikinci return false’da duruyor — "sifir adet" ile aynı satırda, farklı bir vaka
olmasına rağmen. Bu, altı vakalı tabloda neyin tekrarlandığını da netleştiriyor: yeni
vaka "bos ad, farkli miktar", tıpkı "negatif adet"in ikinci satırı "sifir adet"
ile paylaşması gibi, ilk satırı "bos ad" ile paylaşıyor — kapsama aracının gözünde bu
paylaşım hiçbir fark yaratmıyor, ikinci bir dokunuş sayılmıyor.
Bu, konunun ölçü ekseninin bu derse özgü sınırı: kapsama aracı bir koşum bilgisi
üretiyor (kaynağa bakarak bilinemez, yalnız çalıştırılarak öğrenilir), ama ürettiği
bilginin kendisi kaba — bir satırın bir kez mi on kez mi koştuğunu, hangi girdilerle
koştuğunu ayırt etmiyor. Beş vakalık tablo zaten Gecerlinin dört erken dönüşünü ve
son true dönüşünü birer kez koşturduğu için kapsama zaten en yüksek noktasında;
altıncı vaka bu noktanın üstüne çıkamıyor, çünkü koşacak yeni bir satır kalmamış. Konunun
kapanış dersi bu sınırı genişletiyor ve kapsamayı tamamen farklı bir vaka kümesiyle
yeniden ölçüyor.
Bu ölçümün üretim tarafındaki karşılığı somut: bir gözden geçirme sırasında “kapsama oranı yüzde yüze çıktı” ifadesi, tabloya son eklenen vakanın gerçekten yeni bir davranışı sınadığı anlamına gelmiyor. Aynı satırı farklı bir girdiyle yeniden koşturan bir vaka, kapsama raporunda hiçbir iz bırakmadan tabloyu büyütüyor — dosya uzuyor, koşum süresi artıyor, ama aracın döndürdüğü sayı aynı kalıyor. Bu yüzden kapsama oranı, bir vakanın değerli olup olmadığına karar vermek için tek başına yeterli bir ölçüt değil; hangi satırın hâlâ hiç koşmadığını görmek için kapsama aracının satır satır döktüğü ayrıntılı rapora bakmak gerekiyor — konunun kapanış dersi tam olarak bu ayrıntıyı okuyor.
Kapsama aracının bu kabalığı bir kusur değil, ne ölçtüğünün doğal bir sonucu. Araç
kaynak metnin hangi satırlarının derlenmiş ikilide karşılık bulduğunu ve koşum sırasında
kaç kez tetiklendiğini izliyor; bir satırın hangi değerle tetiklendiğini izlemek,
aracın tuttuğu veriyi bambaşka bir düzeye taşırdı. Gecerli gibi kısa, dallanması az bir
işlevde bu kabalık beş vakayla bile en yüksek orana ulaşmayı kolaylaştırıyor; daha çok
dallanan bir işlevde aynı oran çok daha fazla vaka gerektirebilir, ama aracın kendisi
her durumda aynı soruyu soruyor: bu satır en az bir kez koştu mu.
Özet
- Bir tablo güdümlü test dosyasındaki vaka sayısı kaynaktan sayılabiliyor; bu derste
vakalardilimindeki beş satır bir araç çağrılmadan okundu. t.Runbir tabloyu alt testlere bölüyor ve her alt testi verilen adla raporluyor; koşan alt test sayısı bu derste vaka sayısıyla birebir eşleşti.- Vaka sayısı kaynaktan, alt test sayısı ise yalnız
go test -vçalıştırılarak okunabiliyor — ikisi aynı sayıya çıksa da geldikleri yol farklı. - Sınırlayıcı ölçüm: aynı satırı koşturan bir vaka eklemek kapsama oranını değiştirmedi; kapsama aracı satırın kaç kez ve hangi değerle koştuğunu değil, yalnız koşup koşmadığını sayıyor.
- Sahte bağımlılık ailesi ve kapsama ölçümü Birim Testi ve Test Odaklı Geliştirme kursunda kuruldu; burada yeniden anlatılmadı, yalnız Go’nun kendi düzeneği üzerinden ölçüldü.
Sonraki Adım
Bu derste bir tablonun vaka sayısıyla koşan alt test sayısının nasıl eşleştiği ve vaka eklemenin
kapsamayı her zaman artırmadığı ölçüldü. İki ölçüm de aynı araclar.Kur ve araclar.Cagir
yordamlarına dayandı; bu yordamlar konunun geri kalanında da değişmeden sürüyor — işleyiciyi
doğrudan çağıran HTTP dersi dışında. Sıradaki ders aynı soruyu ağ katmanına taşıyor: bir sunucunun
isteğe verdiği yanıt da yalnız koşturularak öğrenilebilen bir bilgi, ama bu kez gerçek bir
bağlantı noktası hiç açılmadan — sunucu ve istemci tarafı test yardımcıları aynı süreç içinde, ağ
hiç devreye girmeden nasıl kurulabiliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.