Go'da test yazmak: tablo testleri (table-driven tests)
Go'nun standart test kütüphanesi ve tablo testi (table-driven test) desenini uygulamalı olarak kavramak; dilin sade test felsefesini benimsemek.
Go ile çalışmaya başladığımda test tarafı ilk şaşırdığım yerlerden biriydi. Test framework’ü yok, assert kütüphanesi yok, expectation sözdizimi yok. Standart kütüphanede testing paketi var; o kadar.
İlk tepkim “bu kadar az?” oldu. Ama birkaç ay sonra tam tersini düşünüyorum: bu sadelik bilinçli bir seçim ve çoğu zaman doğru bir seçim.
Go’da test dosyası nasıl görünür
Go’da testler _test.go uzantılı dosyalara gider. Paket adı genellikle foo_test olur (kara kutu testi için) ya da aynı paketi kullanırsınız (beyaz kutu için). go test ./... komutu bu dosyaları otomatik bulur ve çalıştırır.
// stringutil/reverse.go
package stringutil
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
Basit bir test:
// stringutil/reverse_test.go
package stringutil_test
import (
"testing"
"github.com/username/project/stringutil"
)
func TestReverse(t *testing.T) {
got := stringutil.Reverse("hello")
want := "olleh"
if got != want {
t.Errorf("Reverse(%q) = %q; want %q", "hello", got, want)
}
}
t.Errorf testi başarısız işaretler ama çalışmaya devam eder. t.Fatalf ise o noktada durur. Bu ayrım önemli: birden çok kontrol yapıyorsanız ve hepsinin sonucunu görmek istiyorsanız Errorf; ilk hatadan sonra devam etmenin anlamı yoksa Fatalf.
Table-driven tests
Tablo testi, Go topluluğunun benimsediği ve Go standart kütüphanesinin kaynak kodunda da yaygın görülen bir desendir. Fikir şu: birden fazla giriş-çıkış çiftini bir struct slice olarak tanımlayıp döngüyle çalıştırmak.
func TestReverse_Table(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"tek harf", "a", "a"},
{"basit kelime", "hello", "olleh"},
{"palindrom", "racecar", "racecar"},
{"boş string", "", ""},
{"unicode", "Merhaba", "abahreM"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := stringutil.Reverse(tt.input)
if got != tt.want {
t.Errorf("Reverse(%q) = %q; want %q", tt.input, got, tt.want)
}
})
}
}
t.Run ile her test case ayrı bir subtest olarak çalışır. go test -run TestReverse_Table/palindrom gibi tek bir subtest’i de çalıştırabilirsiniz.
Bu desenin değeri ne?
Okunabilirlik. Tüm durumlar tek bir yerde listeleniyor. Yeni bir durum eklemek bir satır tablo kaydı.
Yalıtım. Her alt test bağımsız; birinin başarısız olması diğerini etkilemiyor.
Tekrar yok. Test mantığı (Reverse çağır, karşılaştır, hata ver) bir kez yazılıyor; varyantlar tablo sağlıyor.
Daha karmaşık durumlar
Sadece giriş-çıkış değil, hata senaryolarını da tablo içine alabilirsiniz:
func TestParseDate_Table(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"geçerli tarih", "2024-05-05", "2024-05-05", false},
{"geçersiz format", "05/05/2024", "", true},
{"boş girdi", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
return
}
if !tt.wantErr && got.Format("2006-01-02") != tt.want {
t.Errorf("ParseDate(%q) = %v; want %v", tt.input, got, tt.want)
}
})
}
}
wantErr bool alanı hata beklenen durumları açıkça işaretliyor. Hem “başarılı yol” hem “hata yolu” aynı tabloda yönetiliyor.
Karşılaştırma için testify?
Go topluluğunda github.com/stretchr/testify kütüphanesi yaygın kullanılan bir alternatif. assert.Equal, assert.NoError gibi yardımcı fonksiyonlar kod miktarını azaltıyor, hata mesajları daha açıklayıcı oluyor.
Ben küçük kütüphaneler ve standart araçlar için sade testing paketini tercih ediyorum. Hata mesajını kendiniz yazmak bir disiplin; ne beklediğinizi ve neyi aldığınızı net ifade etmek zorunda kalıyorsunuz. Bağımlılık eklemeden önce standart kütüphanenin yeterli olup olmadığına bakmak Go’nun genel felsefesiyle örtüşüyor.
Test çalıştırma
# tüm testler
go test ./...
# belirli paket
go test ./stringutil/...
# kapsamlı çıktı
go test -v ./...
# tek test
go test -run TestReverse ./stringutil/...
# kapsam raporu (coverage)
go test -cover ./...
-race bayrağı race condition’ları tespit eder; eşzamanlılık içeren kodda çalıştırmak iyi bir alışkanlık.
Sade testin değeri
PHPUnit, Jest, RSpec — bunların sağladığı kolaylıklara alışkınsanız Go’nun yaklaşımı başta eksik görünüyor. Ama bu sadelik bir trade-off: dış bağımlılık yok, araç değiştiğinde test kodu değişmiyor, ne yaptığınız açık. Çerçeve sizi yönlendirmiyor; tasarım kararları size kalıyor.
Tablo testleri bu sadeliğin içinden çıkmış bir desen. Kütüphane değil, bir kodlama alışkanlığı. Ve bir kez benimseyince başka dillerde de aklınıza geliyor; PHPUnit’te bir dataProvider yazarken artık aynı zihinsel modeli kullanıyorum. Bir test desenini bir dilden öğrenip diğerine taşıyabilmek, polyglot çalışmanın görünmeyen bir kazancı. Go’nun test araçları sade; ama bu sadelik sizi test tasarımının kendisi üzerine düşünmeye ve kendi düzeninizi kurmaya itiyor — hazır kolaylıkların çoğu zaman gizlediği bir beceri.