İçeriğe geç
academia.sh

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 vakalar dilimindeki beş satır bir araç çağrılmadan okundu.
  • t.Run bir 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat