in Go Lang

Golang’de Unit Test

gopher-pilot

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.

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.

test-first

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 :)

[Podcast] PHP 7 Üzerine

MaestroPanel’in bir Podcast kanalı var (itunes). Sektörden haberleri paylaşmak için açtığımız bir kanal. Kurumsal olarak açtık fakat ben kafama göre konuşuyorum, bir nevi hobi edindim kendime. Tabi zaman ve motivasyon buldukça düzensiz yayın yapabiliyoruz.

Son olarak PHP7 desteğini MaestroPanel’e eklerken edindiğimiz tecrübeler üzerine biraz konuştum. Dinlemek için aşağıdan takılın.

Orjinal sayfa: https://wiki.maestropanel.com/maestropanel-radar-6-php-7/

Podcast’i nasıl çekiyorum:

  1. Kayıt cihazı olarak iPhone 4
  2. Sesi editlemek için Audacity yazılımı
  3. iTunes ve Android’de yayınlamak için ise Blubrry PowerPress eklentisini kullanıyorum.

Geçen elemanın biri, podcast kayıtlarımı dinleyip bana ulaştı. “Bende Podcast yapmayı düşünüyorum şu 300$’lık mikrofonlardan hangisini önerirsin diye” sordu. Ona yukarıdaki listeyi küfür ederek gönderdim.

Genelde etrafta görüyorum, koşmak için istediği ayakkabının mağazaya gelmesini bekliyor. Blog yazacak güzel tema bulamadığından şikayet ediyor. Bir girişime başlayacak işten çıkmak istemiyor. Tavsiye ile karışık vaaz vermeyeceğim tabi de, bu tipler fazla artmaya başladı. Belki internetin kullanımı ile ilgilide olabilir…
Sosyologlar incelesin. :]

O değilde yazı nasıl başladı, nasıl bitti arkadaş ya!