Error types

Kendi hata tiplerinizi oluşturmak kodunuzu düzenlemenin zarif bir yolunu oluşturabilir, kodunuzu kullanmayı ve test etmeyi daha kolay hale getirebilir.
Gopher Slack'ten Pedro soruyor:
Eğer fmt.Errorf("%s must be foo, got %s", bar, baz) gibi bir hata oluşturursam, string değerlerini karşılaştırmadan eşitliği test etmenin bir yolu var mı?
Bu fikri keşfetmeye yardımcı bir fonksiyon oluşturalım:
// DumbGetter will get the string body of url if it gets a 200
func DumbGetter(url string) (string, error) {
res, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("problem fetching from %s, %v", url, err)
}
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("did not get 200 from %s, got %d", url, res.StatusCode)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body) // ignoring err for brevity
return string(body), nil
}
Farklı nedenlerle başarısız olabilecek bir fonksiyon yazmak nadir bir durum değildir ve her senaryoyu doğru şekilde ele aldığımızdan emin olmak istiyoruz.
Pedro'nun dediği gibi, durum hatası için şu şekilde bir test yazabiliriz:
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusTeapot)
}))
defer svr.Close()
_, err := DumbGetter(svr.URL)
if err == nil {
t.Fatal("expected an error")
}
want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot)
got := err.Error()
if got != want {
t.Errorf(`got "%v", want "%v"`, got, want)
}
})
Bu test her zaman StatusTeapot döndüren bir sunucu oluşturur ve ardından DumbGetter URL'sini argüman olarak kullanırız. Böylece 200 olmayan yanıtları doğru şekilde işlediğini görebiliriz.

Bu test etme yönteminin sorunları

Bu kitap, testlerinizi dinleyin'i vurgulamaya çalışıyor ve bu test iyi hissettirmiyor:
  • Bunu test etmek için, üretim kodunun yaptığı gibi aynı string'i oluşturuyoruz.
  • Bu okumak ve yazmak için rahatsız edici.
  • Tam hata mesajı string'i, aslında ilgilendiğimiz şey mi?
Bu bize ne anlatıyor? Testimizin ergonomisi, kodumuzu kullanmaya çalışan başka bir kod parçasına yansıyacaktır.
Kodumuzun kullanıcısı, döndürdüğümüz belirli hata türlerine nasıl tepki veriyor? Yapabilecekleri en iyi şey, son derece hataya meyilli ve yazması korkunç olan hata mesajına bakmak.

Ne yapmalıyız

TDD ile aşağıdaki düşünce yapısına girmenin faydasına sahip oluruz:
Ben bu kodu nasıl kullanmak isterdim?
DumbGetter için yapabileceğimiz şey, kullanıcıların tip sistemini kullanarak ne tür bir hata oluştuğunu anlamaları için bir yol sağlamaktır.
Peki DumbGetter bize, aşağıdakilere benzer bir şey döndürebilse:
type BadStatusError struct {
URL string
Status int
}
Büyülü bir string yerine, çalışmak için gerçek verilere sahibiz.
Mevcut testimizi bu ihtiyacı karşılayacak şekilde değiştirelim:
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusTeapot)
}))
defer svr.Close()
_, err := DumbGetter(svr.URL)
if err == nil {
t.Fatal("expected an error")
}
got, isStatusErr := err.(BadStatusError)
if !isStatusErr {
t.Fatalf("was not a BadStatusError, got %T", err)
}
want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}
if got != want {
t.Errorf("got %v, want %v", got, want)
}
})
BadStatusError hata arabirimi uygulayacak şekilde değiştirmemiz gerekecek:
func (b BadStatusError) Error() string {
return fmt.Sprintf("did not get 200 from %s, got %d", b.URL, b.Status)
}

Test ne yapar?

Tam hata string'ini kontrol etmek yerine hatanın bir BadStatusError olup olmadığını görmek için bir tür belirtimi (type assertion) yapıyoruz. Belirtimin geçtiğini varsayarak, hatanın özelliklerinin doğru olduğunu kontrol edebiliriz.
Testi çalıştırdığımızda, bize doğru türde hata döndürmediğimizi söylüyor:
--- FAIL: TestDumbGetter (0.00s)
--- FAIL: TestDumbGetter/when_you_dont_get_a_200_you_get_a_status_error (0.00s)
error-types_test.go:56: was not a BadStatusError, got *errors.errorString
Hata işleme kodumuzu kendi türümüzle güncelleyerek DumbGetter'ı düzeltelim:
if res.StatusCode != http.StatusOK {
return "", BadStatusError{URL: url, Status: res.StatusCode}
}
Bu değişiklik bazı gerçek olumlu etkiler yarattı:
  • DumbGetter işlevimiz daha basit hale geldi, artık bir hata string'inin incelikleri ile ilgilenmiyor, sadece bir BadStatusError oluşturuyor.
  • Testlerimiz artık kodumuzun kullanıcısının, yalnızca günlüğe kaydetme dışında daha karmaşık hata işleme yapmaya karar vermesi durumunda neler yapabileceğini yansıtıyor (ve belgeliyor). Sadece bir tür onayı yapın ve ardından hatanın özelliklerine kolayca erişin.
  • Bu halen "sadece" bir error olduğu için, istediklerinde bunu call stack'e geçirebilirler veya diğer error'lar gibi kaydedebilirler."

Özetlersek

Eğer kendinizi birçok hata durumunu test ederken bulursanız, hata mesajlarını karşılaştırma tuzağına düşmeyin.
Bu, istikrarsız ve okuması/yazması zor testlere yol açar ve kodunuzu kullananların, meydana gelen hataların türüne bağlı olarak işleri farklı şekilde yapmaya başlamaları gerektiğinde karşılaşacakları zorlukları yansıtır.
Testlerinizin her zaman kodunuzu nasıl kullanmak istediğinizi yansıttığından emin olun. Bu nedenle, kendi hata türlerinizi özetlemek için hata türleri oluşturmayı düşünün. Bu, kodunuzu kullanan kullanıcılar için farklı türde hataların ele alınmasını kolaylaştırır ve aynı zamanda hata işleme kodu yazmayı daha basit ve okunması kolay hale getirir.
## Ek Bilgi
Go 1.13'ten itibaren, Go Blog'da standart kütüphanede hatalarla çalışmanın yeni yolları ele alınmaktadır.
t.Run("when you don't get a 200 you get a status error", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusTeapot)
}))
defer svr.Close()
_, err := DumbGetter(svr.URL)
if err == nil {
t.Fatal("expected an error")
}
var got BadStatusError
isBadStatusError := errors.As(err, &got)
want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}
if !isBadStatusError {
t.Fatalf("was not a BadStatusError, got %T", err)
}
if got != want {
t.Errorf("got %v, want %v", got, want)
}
})
Bu durumda, hatayı denemek ve çıkarmak için errors.As kullanıyoruz ve hatamızı custom type'ımıza çıkarıyoruz. Bu, başarı durumunu temsil etmek için bir bool döndürür ve bizim için got içine çıkartır.
Bu sayfa @rasimthegrey tarafından çevrildi.