Go programlama dilinde debugging olayı biraz zayıf olmasından mütevellit çoğu zaman println maymunluğu ile debugging yapar buluyorum kendimi. Hal böyle olunca Unit Test‘in önemini anlıyor developer insanı.
Özelikle bir kaç geniş çaplı proje bitirdikten sonra (biri MaestroPanel) SOLID‘in ve test edilebilir kod (Test First) yazmanın önemini (Bu kitaba bakın) seve seve kavratıyor acı tecrübeler.
Daha önce C#’da yazılım geliştirdiğimden Unit Test, Generation Test ve hatta Functional Test yazmışlığım, uçmuşluğum var ama Golang’da test nasıl yazılır ve yönetilir akabinde maintain edilir yeni öğreniyorum.
Golang’da Unit Test, direkt dil ile birlikte gelen testing paketi ile yazılabiliyor. Sadece Golang’de aşina olduğumuz espirileri 3-5 dakikada öğrenerek projenizi TDD götürebilirsiniz. Önemli noktaları ve araçları aşağıda açıklamaya çalıştım.
Testing Paketi
Golang, test yazabilmeniz için size testing paketini sunuyor. Kodunuzun başına import ettikten sonra yazmaya başlayabiliyorsunuz.
import "testing"
Basit bir test yazmak gerekirse;
package math import "testing" func TestAverage(t *testing.T) { var v float64 v = Average([]float64{1,2}) if v != 1.5 { t.Error("Expected 1.5, got ", v) } }
Burada Average fonksiyonumuza girilen parametreler sonucunda dönen değer 1.5’den farklı ise t.Error() ile test’in fail edeceğini belirtiyoruz.
Dikkat ederseniz testin kontrolünü if koşulu ile sağladık. Bu durum test yazdıkca büyük eziyet olabiliyor. Esasen diğer dillerden de aşina olduğumuz bir assertion mekanizmasına gereksinim duyuyorum ki bakımı kolay, otomasyona musait ve okuması kolay testler yazabileyim;
assert.Equal(expected,v)
Gel gelelim yukarıda olduğu gibi doğrulamayı test paketi ile yapamıyoruz. Bunun için üçüncü parti bir assert kütüphanesi (Assertion Library) kullanmanız mecburi. Aşağıda bu kütüphanelerden işe yarayanları listeledim.
- https://github.com/stretchr/testify
- http://labix.org/gocheck
- https://github.com/franela/goblin
- http://onsi.github.io/ginkgo/
- http://onsi.github.io/gomega/
Dosya İsimlendirmesi
Golang’de test yazacaksanız öncelikle yeni bir dosya oluşturup sonunun *_test.go ile bitmesini sağlamanız gerekiyor. Yani default davranış bu şekilde.
Teamul ise şöyle;
Örneğin main.go dosyası içindeki fonksiyonların testini yazmak için,
main_test.go
isminde bir dosya oluşturmalısınız. Go test tool’u direkt *_test.go ile biten dosyaları dikkate alarak içindeki test fonksiyonlarını çalıştırıyor.
Fonksiyon İsimlendirmesi
Dosya isimlendirmesi nasıl test ile bitiyorsa Unit Test’lerin isimleri Test ile başlamak zorunda.
Örneğin:
func TestXxx(*testing.T)
Burada test’in baş harfinin büyük (Public) olması önemli. Küçük harf yaparsanız Private olarak işaretlendiğinden işler zorlaşacaktır.
Test Çalıştırmak
Go’da testleri çalıştırmak için “go test” komut satırı aracını kullanıyoruz. Bu araç ile ilgili pratikleri aşağıda bulabilirsiniz.
Tüm parametreleri teker teker vermeyeceğim tabi. Merak ediyorsanız tıklayın veya “go help testflag” komutunu verin.
Şimdi sık kullanılan test senaryoları “go test” aracı ile nasıl çalıştırılır bir bakalım.
İlgili klasördeki tüm test’leri çalıştırmak için, terminalden projenin klasörüne geçip aşağıdaki komutu çalıştırın.
go test -v
-v parametresi Verbose etmesi için yani test isimlerini,zaman vb. şeyleri ekrana basması için ekleniyor. Daha sade çıktı istiyorsanız vermeyebilirsiniz.
-race parametresi de önemli.
Örneğin testlerinizde goroutine kullanıyorsunuz ve bu rutinlerin aynı anda aynı memory alanına erişip erişmediğini -race parametresi ile öğrenebiliyorsunuz, aklınızda bulunsun.
Testlerin düzgün çalışması için bazen dışarıdan dosya okumanız gerekebiliyor. Bu tarz dosyaları “testdata” isminde bir klasör içine koyarsanız “go test” aracı bu klasörü görmezlikten gelir.
Sadece belirli klasördeki testleri çalıştırmak için;
go test ./src/client -v
Sadece belirli bir logic_test.go dosyasındaki testleri çalıştırmak için;
go test logic_test.go
Bir kaç test dosyasını çalıştırmak için;
go test logic_test.go mogic_test.go ./src/client/magic_test.go
Tek bir testi çalıştırmak için;
go test -run TestName
Bir paketin içindeki testi çalıştırmak için;
go test packagename -run TestName
-run paremteresinin değeri aslında Regexp (Regular Expression) tipinden verilebiliyor, buna göre filitreleme rahatlıkla yapabilirsiniz. Help dosyasındaki açıklaması aşağıdaki gibi.
-run regexp
Run only those tests and examples matching the regular expression. For tests the regular expression is split into smaller ones by top-level ‘/’, where each must match the corresponding part of a test’s identifier.
Örneğin;
Test dosyanızın içinde onlarca test var ve siz sadece isminde API geçen testleri çalıştırmak istiyorsunuz.
go test all_test.go -run API
veya,
İsminin sonunda ByID geçen testleri çalıştırmak istiyorsunuz.
go test all_test.go -run ByID$
veya,
isminin başında Create olan testleri çalıştırmak istiyorsunuz.
go test all_test.go -run ^TestCreate
Yukarıdaki gibi Regex ile filitre verilebiliyor.
TestMain (Go 1.4’den Sonra)
Golang’de yazdığınız testler için main() fonksiyonu işlevi gören bir özellik var, TestMain.
TestMain fonksiyonu testlerinizi otomatik olarak çalıştıran ve testler üzerinde daha fazla kontrol sağlayan bir özellik.
Örneğin, başka bir testin çıktısını başka bir testte kullanmanız gerektiğinde veya testlerin belirli bir sırada çalışması gerektiğinde ya da testte bir şeyler otomatikleştirmek istediğinizde TestMain özelliğine başvurabilirsiniz.
Örnek:
package example_test import ( "os" "testing" ) func TestA(t *testing.T) { } func TestB(t *testing.T) { } func setup() { println("setup") } func teardown() { println("teardown") } func TestMain(m *testing.M) { setup() ret := m.Run() if ret == 0 { teardown() } os.Exit(ret) }
Burada “go test example_test” dediğinizde direkt TestMain içinden önce setup fonksiyonu çalıştırılıyor. Hemen ardından m.Run() ile TestA ve TestB testlerinin çalıştırılması sağlanıyor, m.Run() sonucu sıfır dönüyorsa’da teardown() metodu çağrılıyor.
Bu arada TestMain fonksiyonu bütün testler için geçerlidir. Yani her halukarda testler başlamadan önce bu fonksiyona girer . Herhangi bir dosyada olması yeterlidir.
Diğer bir trick’de os.Exit() ile geri döndürmelisiniz ki sıfır döndüğünde testiniz fail görünmesin.
Subtests (Go 1.7 den sonra)
Code Kata yaparken öyle bir baktım sadece, kavramsal olarak çok anlayamadım açıkcası ama değinmeden geçmek istemedim. Lazım olur.
Subtest ile standart bir Unit Test’in içinde ayrı bir test senaryosu işletebiliyorsunuz. Örnek vermek gerekirse;
func TestBirSeylerYap(t *testing.T) { t.Run("A=1", func(t *testing.T) { }) t.Run("A=2", func(t *testing.T) { }) t.Run("YaptigimiYapma", func(t *testing.T) { }) }
TestBirSeylerYap testinin içinde “A=1” isminde subtest oluşuturup func tipindeki parametreye test yazabiliyoruz. Bu örnekte boş bırakılmış tabi, o boşluk sizde artık.
Bir Subtest’i “go test” ile çalıştırıyoruz. Aşağıdaki komut TestBirSeylerYap testinin içindeki YaptigimiYapma subtestini çalıştırıyor.
go test -run TestBirSeylerYap/YaptigimiYapma
Bütün standart testlerin altındaki A= ile başlayan Subtest’leri çalıştır da denilebiliyor. Aşağıdaki gibi;
go test -run /A=
Özetle hiyerarşik senaryolarda (ne demekse) veya bir grup fonksiyonun çalıştırılması ile alakalı senaryolar için işlevsel olabilecek bir özellik.
Test Coverage
Test üzerine bir şeyler yazmışken olayı tamamlaması açısından Test Coverage olayına da değinmekte fayda var.
Test Coverage’in anlatmayacağım tabi. Daha çok Golang tarafında Code Coverage’inizi nasıl hesaplatacağınızı belirtmek amacım.
Testlerinizi yazdınız, hepsi “PASS” geçiyor, böyle yem yeşil. Peki projenin ne kadarını test ettiniz? ne kadar eksiğiniz var? bulmak için aşağıdaki komutu kullanabilirsiniz.
go test -coverprofile cover.out
aşağıdaki gibi bir çıktı verir.
PASS coverage: 60.0% of statements
Genel bir görüş olarak coverage değeri %80’i buldumu kafi olarak kabul edilir ve fazla kurcanlanmaz. Bu nereden çıkmış bilmiyorum ama öyle olsa bile geri kalan %20’lik kısmın hala nasıl tepki vereceğini bilmediğiniz anlamına geliyor. Bunuda unutmamak lazım.
Bir yandan da şu var.
Siz %100 Code Coverage’e erişseniz bile, sanmayınki uygulamanız süper çalışacak! (Keşke sadece Unit Test yazarak o kaliteyi yakalayabilsek). Her halukarda kodunuz patlayacak ve değiştireceksiniz.
Unit Test’ler aslında aptal şeylerdir, sadece kodunuz yanlış çalışırsa size haber verir dolayısılya kodunuzun nasıl çalıştığını test etmek diğer test türlerini kullanmanız gerekir bunuda mesaj olarak vereyim.
Mock
Golang ile beraber bir mock object kütüphanesi gelmiyor (sanırım http için vardı) fakat yine native kendi fake objeleriniz ile bir çok yerden yırtmanız mümkün.
Gel gelelim yırtamadığınız yerlerde üçüncü parti silahları kullanmanız lazım. Aşağıda bir iki Mocking Framework’ünü ekledim.
Bende Mock kavramını keşfedene kadar veritabanı fonksiyonunun testini yazdığımda gidip hakkatten yeni satır ekletiyordum. Yada bir HTTP API testi yazdığımda gerçekten gidip işlemin sonucunu doğruluyordum bunu da itiraf edeyim! :)
Mock, aslında başlı başına bir sanat bazen proje task’larından çok kafa yorabiliyor adam.
Test First
Aslında Unit Test’te olay Test First‘tür yani Test Driven Development yani ilk önce belirlediğiniz özelliğin test’ini yazarak projeye başlarsınız. Önce testi yazmaya başladığınızda otomatikman kodunuzda test edilebilir şekilde çıkar, en nihayetinde sürekli değişime açık, esnek ve kolay yönetebileceğiniz bir projeniz oluşur. Özellikle Extreme Programming’de baya önemsenir bu konu.
Eğer Unit Testi sonradan yazarsanız bu TDD olmaz, TDD But! olur (Scrum-But’dan çaldım). Firmalarda sürekli TDD gidicez diyerek gazladıkları ekibin, sonradan “ya abi çok yoruyor bizi” diyerek vazgeçmelerinin sebebi tam da bu.
Açıkcası Test First benimsenmeden TDD gitmenizin bir anlamı yok! Bunu yapabilmeniz için yazılım geliştirme kültürünüzü değiştirmeniz şart.
Burada Software Craftmanship (manifestosu) kavramı öne çıkıyor yani siz mesleki açıdan ilerlemek istiyorsanız bunun gibi konuları benimsemeniz ve teknik borçlarınızı ödemeniz gerekiyor.
Bu yükü omuzlarınıza yükledikten sonra makaleyi bitiriyorum :)
Bonus
Go’da daha efektif test için Table Driven Tests’e bir göz atın: https://github.com/golang/go/wiki/TableDrivenTests akabinde https://blog.golang.org/subtests
Awsome Go çok güzel repo. Sık güncelleniyor, her baktığımda yeni bir şey keşfediyorum https://github.com/avelino/awesome-go
Testleri browser üzerinden yönetebileceğiniz ve çalıştırabileceğiniz güzel bir araç goconvey.co
Mitchell Hashimoto’nun “Advanced Testing With Go” sunumu kafa açıcı. Hashicorp’un OSS ürünlerini “okumanızı” öneririm: https://github.com/hashicorp
Gabe Rosenhouse‘ın TDD ile ilgili videosu DI üzerinde de fikir veriyor. Ginko ve Gomega içerir.
Berna Gökçe‘nin Agouti, Ginkgo, Gomega gibi araçları anlattığı video.
Video’da bahsi geçen güzel araçlar var, seyrederseniz yakalarsınız zaten ama aşağıdaki aracı yazmadan geçmemeyim.
HTTP Mocking yapmak için başka bir tool ise mmock
Dave Cheney’in Go uygulamanızı nasıl Profile edeceğinizi anlatan videosu. pprof, perf, go trace güzelliklerini analtıyor.
Profiling minvalinde gops (https://github.com/google/gops) denen bir araç var, lazım olursa.
Bu arada ben böyle videoları x1.5 veya x2 hızlandırılmuş izliyorum zamandan tasarruf için. Diğer yandan konsantre olmayı kolaylaştırıyor ya da ben öyle sanıyorum :)
Neden golang? Ben yeni hobi dili arayışı icindeyim ruby sıcak geldi ancak .net dev. Yaptığınız icin de sorayim dedim. Sizce neden go lang
Benim açımdan daha çok çözüm geliştirdiğin alan ile alakalı. Öncelikle cross platform olması benim avantajıma. hızlı geliştirme yapabiliyorsun, taşınabilir, performanslı ve sonuç odaklı bir dil.
Handikapları yok değil tabi OOP bazında ilkel, bazı durumlarda yetersiz kalıyor (adapte oluyorsun), 3rd party tool’lar zayıf (daha fazla öğrenmeye itiyor)
Hobi olarak Ruby bence doğru karar. Crystal (crystal-lang.org) ‘e de bakmanı öneririm.
Çok açıklayıcı bir yazı, teşekkürler. İyi test edilmiş projelerin bir listesine bakındım awesome-go porjesinde ama bulamadım, öyle bir liste de olsa bence faydalı olurdu.
Minio projesi test konusunda incelenesi https://github.com/minio/minio
Docker projeside örnek değil ama goway bir teknik izlemişler test ile alakalı.