Go Uygulamanızı Test Etme: Doğru Yoldan Başlayın
Yayınlanan: 2022-03-11Yeni bir şey öğrenirken, yeni bir zihin durumuna sahip olmak önemlidir.
Go'da oldukça yeniyseniz ve JavaScript veya Ruby gibi dillerden geliyorsanız, alay etmenize, onaylamanıza ve diğer test sihirbazlıklarını yapmanıza yardımcı olan mevcut çerçeveleri kullanmaya alışmışsınızdır.
Şimdi dış bağımlılıklara veya çerçevelere güvenme fikrini ortadan kaldırın! Test etme, birkaç yıl önce, çok daha az kaynağın olduğu bir zamanda, bu olağanüstü programlama dilini öğrenirken karşılaştığım ilk engeldi.
Go'da test başarısının, bağımlılıklara (Go'daki her şeyde olduğu gibi) ışık tutmak, dış kitaplıklara minimum düzeyde güvenmek ve yeniden kullanılabilir iyi kod yazmak anlamına geldiğini artık biliyorum. Blake Mizerany'nin üçüncü taraf test kitaplıklarıyla ortaya çıkan deneyimlerinin bu sunumu, zihniyetinizi ayarlamak için harika bir başlangıçtır. Harici kütüphaneleri ve çerçeveleri kullanmakla "Gitme yolu" yapmakla ilgili bazı iyi argümanlar göreceksiniz.
Kendi test çerçevenizi ve alaycı konseptlerinizi oluşturmak mantıksız görünebilir, ancak düşünüldüğünden daha kolaydır ve dili öğrenmek için iyi bir başlangıç noktasıdır. Ayrıca, öğrendiğim zamandan farklı olarak, yaygın test senaryolarında size rehberlik edecek ve ayrıca verimli bir şekilde test etmek ve kodu temiz tutmak için en iyi uygulamaları düşündüğüm teknikleri tanıtmak için bu makaleye sahipsiniz.
Go'da Tablo Testi
Temel test birimi - 'birim testi' şöhretinin - bir girdi alan ve bir çıktı döndüren en basit biçimindeki bir programın herhangi bir bileşeni olabilir. Testlerini yazmak istediğimiz basit bir fonksiyona bir göz atalım. Hiçbir yerde mükemmel veya eksiksiz değil, ancak gösteri amaçları için yeterince iyi:
avg.go
func Avg(nos ...int) int { sum := 0 for _, n := range nos { sum += n } if sum == 0 { return 0 } return sum / len(nos) }
Yukarıdaki işlev, func Avg(nos ...int)
, kendisine verilen bir dizi sayının sıfır veya tamsayı ortalamasını döndürür. Şimdi bunun için bir test yazalım.
Go'da, test edilen kodu içeren dosyayla aynı ada sahip bir test dosyasını, eklenen _test
son ekiyle adlandırmak en iyi uygulama olarak kabul edilir. Örneğin, yukarıdaki kod avg.go adlı bir avg.go
, bu nedenle test dosyamız avg_test.go
olarak adlandırılacaktır.
Paket tanımı ve içe aktarma işlemleri basitlik amacıyla çıkarıldığından, bu örneklerin yalnızca gerçek dosyalardan alıntılar olduğunu unutmayın.
Avg
. işlevi için bir test:
avg__test.go
func TestAvg(t *testing.T) { for _, tt := range []struct { Nos []int Result int }{ {Nos: []int{2, 4}, Result: 3}, {Nos: []int{1, 2, 5}, Result: 2}, {Nos: []int{1}, Result: 1}, {Nos: []int{}, Result: 0}, {Nos: []int{2, -2}, Result: 0}, } { if avg := Average(tt.Nos...); avg != tt.Result { t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg) } } }
İşlev tanımıyla ilgili dikkat edilmesi gereken birkaç nokta vardır:
- İlk olarak, test işlevi adındaki 'Test' öneki. Bu, aracın onu geçerli bir test olarak algılaması için gereklidir.
- İşlev adının sonraki kısmı genellikle test edilen işlevin veya yöntemin adıdır, bu durumda
Avg
. - Ayrıca test akışının kontrolünü sağlayan
testing.T
adlı test yapısını da geçmemiz gerekiyor. Bu API hakkında daha fazla ayrıntı için lütfen dokümantasyon sayfasını ziyaret edin.
Şimdi örneğin yazıldığı formdan bahsedelim. Avg()
işlevi aracılığıyla bir test paketi (bir dizi test) yürütülüyor ve her test belirli bir girdi ve beklenen çıktı içeriyor. Bizim durumumuzda, her test bir dilim tamsayı ( Nos
) gönderir ve belirli bir dönüş değeri ( Result
) bekler.
Golang Arayüzü Alaycılığı
Go dilinin sunduğu en büyük ve en güçlü özelliklerden birine arayüz denir. Arayüzleme, programlarımızı tasarlarken arayüz oluşturmadan elde ettiğimiz güç ve esnekliğin yanı sıra, bileşenlerimizi ayrıştırmak ve onları buluşma noktalarında kapsamlı bir şekilde test etmek için de bize harika fırsatlar sunuyor.
Bir io.Reader'dan ilk N baytı okumamız ve bunları bir dizge olarak döndürmemiz gereken hayali bir senaryo alalım. Şuna benzer bir şey olurdu:
readn.go
// readN reads at most n bytes from r and returns them as a string. func readN(r io.Reader, n int) (string, error) { buf := make([]byte, n) m, err := r.Read(buf) if err != nil { return "", err } return string(buf[:m]), nil }
Açıkçası, test edilecek ana şey, çeşitli girdiler verildiğinde readN
işlevinin doğru çıktıyı döndürmesidir. Bu, tablo testi ile yapılabilir. Ancak, şunları kontrol etmemiz gereken, önemsiz olmayan iki husus daha var:
-
r.Read
, n boyutunda bir arabellek ile çağrılır. -
r.Read
, atılırsa bir hata döndürür.
r.Read
iletilen arabelleğin boyutunu bilmek ve döndürdüğü hatayı kontrol altına almak için, readN
iletilen r
ile alay etmemiz gerekir. Reader türündeki Go belgelerine bakarsak, io.Reader
nasıl göründüğünü görürüz:
type Reader interface { Read(p []byte) (n int, err error) }
Bu oldukça kolay görünüyor. io.Reader
tatmin etmek için tek yapmamız gereken kendi Read
yöntemimize sahip olmak. Dolayısıyla ReaderMock
aşağıdaki gibi olabilir:
type ReaderMock struct { ReadMock func([]byte) (int, error) } func (m ReaderMock) Read(p []byte) (int, error) { return m.ReadMock(p) }
Yukarıdaki kodu biraz analiz edelim. Herhangi bir ReaderMock
örneği, gerekli Read
yöntemini uyguladığı için io.Reader
arabirimini açıkça karşılar. Taklitimiz ayrıca, alaylı yöntemin tam davranışını ayarlamamıza izin veren ReadMock
alanını da içerir, bu da ihtiyacımız olan her şeyi dinamik olarak başlatmamızı çok kolaylaştırır.
Arayüzün çalışma zamanında tatmin edilmesini sağlamak için hafızasız harika bir numara, kodumuza aşağıdakileri eklemektir:
var _ io.Reader = (*MockReader)(nil)
Bu, iddiayı kontrol eder, ancak herhangi bir şey ayırmaz, bu da, program gerçekten onu kullanan herhangi bir işlevselliğe girmeden önce , derleme zamanında arayüzün doğru şekilde uygulandığından emin olmamızı sağlar. İsteğe bağlı bir numara, ancak yardımcı olur.
Devam edelim, r.Read
n
boyutunda bir tamponla çağrıldığı ilk testimizi yazalım. Bunu yapmak için ReaderMock
aşağıdaki gibi kullanıyoruz:
func TestReadN_bufSize(t *testing.T) { total := 0 mr := &MockReader{func(b []byte) (int, error) { total = len(b) return 0, nil }} readN(mr, 5) if total != 5 { t.Fatalf("expected 5, got %d", total) } }
Yukarıda görebileceğiniz gibi, “fake” io.Reader
Read
işlevi için davranışını, daha sonra testimizin geçerliliğini doğrulamak için kullanılabilecek bir kapsam değişkeni ile tanımladık. Yeterince kolay.
Test etmemiz gereken, bir hata döndürmek için Read
ile alay etmemizi gerektiren ikinci senaryoya bakalım:
func TestReadN_error(t *testing.T) { expect := errors.New("some non-nil error") mr := &MockReader{func(b []byte) (int, error) { return 0, expect }} _, err := readN(mr, 5) if err != expect { t.Fatal("expected error") } }
Yukarıdaki testte, mr.Read
(alaylı Okuyucumuz) yapılan herhangi bir çağrı, tanımlanan hatayı döndürecektir, bu nedenle readN
doğru işleyişinin de aynı şeyi yapacağını varsaymak güvenlidir.
Go ile Fonksiyon Alaycılığı
Genellikle bir işlevle alay etmemiz gerekmez, çünkü bunun yerine yapıları ve arabirimleri kullanma eğilimindeyiz. Bunları kontrol etmek daha kolaydır, ancak bazen bu zorunlulukla karşılaşabiliriz ve bu konuda sık sık kafa karışıklığı görüyorum. Bazı insanlar log.Println
gibi şeylerle nasıl alay edileceğini bile sordular. Nadiren log.Println
için verilen girişi test etmemiz gerekmesine rağmen, bu fırsatı göstermek için kullanacağız.

Aşağıdaki, n
değerine bağlı olarak çıktıyı günlüğe kaydeden bu basit if
ifadesini düşünün:
func printSize(n int) { if n < 10 { log.Println("SMALL") } else { log.Println("LARGE") } }
Yukarıdaki örnekte, log.Println
doğru değerlerle çağrıldığını özellikle test ettiğimiz saçma senaryoyu varsayıyoruz. Bu fonksiyonla alay edebilmemiz için önce onu kendi içimize sarmamız gerekiyor:
var show = func(v ...interface{}) { log.Println(v...) }
Fonksiyonu bu şekilde - bir değişken olarak - bildirmek, testlerimizde üzerine yazmamıza ve istediğimiz davranışı atamamıza izin verir. Örtük olarak, log.Println
atıfta bulunan satırlar show
ile değiştirilir, böylece programımız şöyle olur:
func printSize(n int) { if n < 10 { show("SMALL") } else { show("LARGE") } }
Şimdi test edebiliriz:
func TestPrintSize(t *testing.T) { var got string oldShow := show show = func(v ...interface{}) { if len(v) != 1 { t.Fatalf("expected show to be called with 1 param, got %d", len(v)) } var ok bool got, ok = v[0].(string) if !ok { t.Fatal("expected show to be called with a string") } } for _, tt := range []struct{ N int Out string }{ {2, "SMALL"}, {3, "SMALL"}, {9, "SMALL"}, {10, "LARGE"}, {11, "LARGE"}, {100, "LARGE"}, } { got = "" printSize(tt.N) if got != tt.Out { t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got) } } // careful though, we must not forget to restore it to its original value // before finishing the test, or it might interfere with other tests in our // suite, giving us unexpected and hard to trace behavior. show = oldShow }
Bizim paket servisimiz 'mock log.Println
' olmamalıdır, ancak bu çok nadir senaryolarda, meşru sebeplerden dolayı paket düzeyinde bir işlevle alay etmemiz gerektiğinde, bunu yapmanın tek yolu (bildiğim kadarıyla) değerini kontrol edebilmemiz için onu paket düzeyinde bir değişken olarak bildirerek.
Ancak, log.Println
gibi şeylerle alay etmemiz gerekirse, özel bir günlükçü kullanacaksak çok daha zarif bir çözüm yazılabilir.
Şablon Oluşturma Testlerine Git
Oldukça yaygın olan diğer bir senaryo, işlenmiş bir şablonun çıktısının beklentilere göre olup olmadığını test etmektir. Aşağıdaki gövdeyi döndüren http://localhost:3999/welcome?name=Frank
için bir GET isteğini ele alalım:
<html> <head><title>Welcome page</title></head> <body> <h1 class="header-name"> Welcome <span class="name">Frank</span>! </h1> </body> </html>
Şimdiye kadar yeterince açık olmaması durumunda, sorgu parametre name
“ad” olarak sınıflandırılan span
içeriğiyle eşleşmesi tesadüf değildir. Bu durumda, bariz test, bunun birden çok çıktıda her seferinde doğru şekilde gerçekleştiğini doğrulamak olacaktır. GoQuery kitaplığını burada son derece yararlı buldum.
Şimdi testimizi şu şekilde yazabiliriz:
welcome__test.go
func TestWelcome_name(t *testing.T) { resp, err := http.Get("http://localhost:3999/welcome?name=Frank") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } doc, err := goquery.NewDocumentFromResponse(resp) if err != nil { t.Fatal(err) } if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" { t.Fatalf("expected markup to contain 'Frank', got '%s'", v) } }
İlk olarak, devam etmeden önce yanıt kodunun 200/OK olup olmadığını kontrol ediyoruz.
Yukarıdaki kod parçacığının geri kalanının kendi kendini açıklayıcı olduğunu varsaymanın çok zor olmadığına inanıyorum: http
paketini kullanarak URL'yi alıyoruz ve yanıttan, daha sonra sorgulamak için kullandığımız yeni bir goquery uyumlu belge oluşturuyoruz döndürülen DOM. h1.header-name
içindeki span.name
'Frank' metnini kapsadığını kontrol ediyoruz.
JSON API'lerini test etme
Go, sıklıkla bir tür API yazmak için kullanılır, bu nedenle son olarak ama en az değil, JSON API'lerini test etmenin bazı üst düzey yollarına bakalım.
Uç noktanın önceden HTML yerine JSON döndürüp döndürmediğini düşünün, bu nedenle http://localhost:3999/welcome.json?name=Frank
yanıt gövdesinin şöyle görünmesini beklerdik:
{"Salutation": "Hello Frank!"}
Önceden tahmin edilmiş olabileceği gibi, JSON yanıtlarını onaylamak, herhangi bir dış kitaplığa veya bağımlılığa ihtiyacımız olmaması dışında, şablon yanıtlarını öne sürmekten çok farklı değildir. Go'nun standart kütüphaneleri yeterlidir. Verilen parametreler için doğru JSON'un döndürüldüğünü onaylayan testimiz:
welcome__test.go
func TestWelcome_name_JSON(t *testing.T) { resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank") if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Fatalf("expected 200, got %d", resp.StatusCode) } var dst struct{ Salutation string } if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil { t.Fatal(err) } if dst.Salutation != "Hello Frank!" { t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation) } }
Karşılaştığımız yapı dışında herhangi bir şey döndürülürse, json.NewDecoder
bunun yerine bir hata döndürür ve test başarısız olur. Yanıtın yapıya karşı başarıyla çözüldüğünü göz önünde bulundurarak, alanın içeriğinin beklendiği gibi olduğunu kontrol ediyoruz - bizim durumumuzda “Merhaba Frank!”.
Kurulum ve Yıkım
Go ile test etmek kolaydır, ancak hem yukarıdaki JSON testinde hem de ondan önceki şablon oluşturma testinde bir sorun vardır. Her ikisi de sunucunun çalıştığını varsayar ve bu güvenilmez bir bağımlılık yaratır. Ayrıca, "canlı" bir sunucuya karşı çıkmak iyi bir fikir değildir.
Neyse ki Go, test sunucuları oluşturmak için httptest paketini sunuyor. Testler, ana sunucumuzdan bağımsız olarak kendi ayrı sunucularını harekete geçirir ve bu nedenle testler üretime engel olmaz.
Bu durumlarda, çalışan bir sunucu gerektiren tüm testler tarafından çağrılacak genel setup
ve teardown
işlevleri oluşturmak idealdir. Bu yeni, daha güvenli modelin ardından, testlerimiz şuna benzer bir sonuca varacaktır:
func setup() *httptest.Server { return httptest.NewServer(app.Handler()) } func teardown(s *httptest.Server) { s.Close() } func TestWelcome_name(t *testing.T) { srv := setup() url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL) resp, err := http.Get(url) // verify errors & run assertions as usual teardown(srv) }
app.Handler()
referansına dikkat edin. Bu, üretim sunucunuzun veya bir test sunucusunun örneğini oluşturabilen uygulamanın http.Handler'ını döndüren en iyi uygulama işlevidir.
Çözüm
Go'da test yapmak, programınızın dış perspektifini üstlenmek ve ziyaretçilerinizin veya çoğu durumda API'nizin kullanıcılarının yerini almak için harika bir fırsattır. Hem iyi kod hem de kaliteli bir deneyim sunduğunuzdan emin olmak için harika bir fırsat sunar.
Kodunuzdaki daha karmaşık işlevlerden emin olmadığınızda, test etmek bir güvence olarak kullanışlı olur ve ayrıca daha büyük sistemlerin parçalarını değiştirirken parçaların birlikte çalışmaya devam edeceğini garanti eder.
Umarım bu makale işinize yaramıştır ve başka test hileleri biliyorsanız yorum yapabilirsiniz.