# 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.

- Yayın: 2024-05-05
- Kategori: Diller
- Etiketler: Go, Test
- Okuma süresi: 4 dk okuma
- Kaynak: https://www.muhammetsafak.com.tr/blog/go-da-test-yazmak-tablo-testleri-table-driven-tests/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
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.

```go
// 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:

```go
// 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.

```go
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ı](/blog/goda-hata-yonetimi-error-degeriyle-yasamak) da tablo içine alabilirsiniz:

```go
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

```bash
# 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](/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi) ç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.
