Go’da Test Yazmak

Selamlar,

Bu yazıda, biraz olsun Test yazmanın öneminden ve Go’da bir kaç test senaryosunda bahsedeceğiz.

Genellikle firmalar, hem Test maliyetlerinden hem de onların gözünden boşa zaman harcamaktan kaçınmak isterler. Öncelikle herkes hata yapabilir. Ya da hiç hata yapılmasa bile, sonradan değişen bussines planlara bağlı olarak kodda yapılan değişikliklerin, nelere maal olabileceği önceden kestirilemeyebilir. Örneğin, yayına bir sene önce alınmış bir Erp projesinde, yeni yayımlanan bakanlık kararı ile bir takım hesaplamaların değişmesi ve bunun koda entegrasyonunun yapılması gerekebilir. Yapılan bu değişimlerin nereleri etkileyebileceği, nerelerin çalışmasını bozabileceği, önceden kestirilemediği gibi, bir hata çıkması durumunda da bunun bulunması bile, başlı başına bir iş planı olabilir. Hele ki üzerinden bir yıl geçmiş ve yapılanlar unutulmaya başlanmış ise :) İşte bunun gibi senaryolarda, Testin önemi bir kez daha ortaya çıkmaktadır. Test başta maliyetli olsa da, projenin devamında yeni featureların eklenmesinde, mütişh zaman kazandırmaktadır. Çünkü manuel yani elle yapılacak kontrollerin tamamı, otomatize edilmektedir. Çok farklı Test tipleri vardır. UI testleri, Performans(Yük) testi, Sistem Testi , Unit(Birim) Testi gibi.

“Bilgiyi deneyimle ve inatla test etmeye bağlı ol ve hatalarından öğrenmeye açık ol.” ―Leonardo Da Vinci

Biz bugün yazılan fonksiyonların düzgün çalışıp çalışmadığını anlamak için, Go’da birkaç Birim Testi yani Unit Test yazacağız.

Öncelikle ben Go’da ide olarak, JetBrains’in GoLand’ini kullanıyorum. Ama siz isterseniz VsCode‘u da, gayet güzel kullanabilirsiniz.

Yukarıda görüldüğü gibi, TestArticle projesi Go’da oluşturulur. Ayrıca Go 1.16 versiyonunu ile çalışıyorsanız ve “go: cannot find main module” hatası alıyorsanız, aşağıdaki komut ile ilgili module (go.mod) dosyası yaratılmış olunur.

account.go:

account.go dosyası aşağıdaki gibi eklenir. Bu makaledeki test senaryosu, hepimizin yakından bildiği ev bütçesi denkleştirme olacaktır :) Giren bir maaş, yapılan harcamalar ve geriye kalan paranın hesaplanması.

Aşağıda “IAccount” interface’i ile, Account Struct’in sahip olması gereken functionlar tanımlanmıştır.

  • State(): Maaşın son durumu.
  • Spending(): Yapılan harcamalar.

  • Account : money şeklinde propertysi olan, banka hesabımız.
  • “NewAccount()” function’ı, money yani maaş değerinin atanıp, Account struct’ının referance ile alındığı, bir çeşit Constructor’dır.

  • State() function: Account’un en son maaş, yani money durumunu bize verir.
  • Spending() function: Account üzerinden harcama yapmayı ve ilgili harcamayı maaşdan yani, paradan düşürmeyi sağlar.

Böylece Account struct, aşağıdaki gibi oluşturulmuş olunur. Amaç test edilebilecek fonksiyonel bir sistemin, oluşturulmasıdır.

account.go(Tüm Kodlar):

account_test.go:

Aşağıda görüldüğü gibi account’un functionlarını test etmek için, aşağıdaki test sayfası “import ‘testing’” kütüphanesi eklenerek yaratılmıştır.

TestNewAccount(): “NewAccount(5000)” function’ı ile, 5000 maaşlı yeni bir account yaratılmış ve “State()” function’ı test edilmiştir. Girilen maaş bilgisi ile State aynı değerde değil ise, “t.Error(”)” ile geriye hata dönülmüştür.

Aşağıda görüldüğü gibi ilgili test, başarı ile geçilmiştir.

“Hayatın, insanın iradesini test etmek için pek çok yolu vardır; bazen hiçbir şey olmaz ya da her şey birden olur.” ―Paulo Coelho

TestNewAccountBySalary(): Aşağıdaki örnekde test datası amacı ile, testGrup adında salary ve state propertyleri olan bir struct [ ] dizisi oluşturulmuştur. “salary” yatırılan maaş, “state” ise alınılması gereken para miktarıdır. Biliyorum bu basit bir test, ama amacım adım adım testleri zorlaştırarak size testin amacını açıklamak :)

  • for _, p := range testGrup” : Herbir test datası, tek tek denenir.
  • a := NewAccount(p.salary)“: Belirlenen maaş miktarı (Test Datası) ile, yeni bir account yaratılır.
  • if a.State() != p.state {“: Atanan maaş tipi ile state, yani para miktarı eşit değil ise hata fırlatılır.

Yapılan Test sonucu aşağıdaki gibidir: Gönderilen test datasında, 4000, 5000’e eşit olmadığı için yani, yatırılan maaş ile paranın o anki miktarı aynı olmadığı için hata fırlatılmıştır. Bu örnekde, özellikle hata alınması amacı ile bazı test dataları yanlış girilmiştir.

TestAccountPayAndState(): Yeni bir maaş versi ile account oluşturulup harcama yapılmakta, ve geri kalan para miktarı kontrol edilip test edilmektedir.

  • testGrup : Test amaçlı “salary” yani maaş, “payment” yani yapılan harcama ve son olarak kalan paranın kontrolünün yapıldığı “state” paramtere dizisi olarak gönderilir.
  • for _, p := range testGrup {” : Herbir test datası tek tek gezilir.
  • a := NewAccount(p.salary)“: Belirlenen test datası miktarında, maaş atanarak yeni bir account yaratılır.
  • a.Spending(p.payment)” : Belirlenen test datası miktarında, harcama yapılır.
  • if a.State() != p.state {“: Geri kalan paranın, olması gereken para miktarı ile aynı olup olmadığına bakılır. Aynı değil ise hata fırlatılır.

Yapılan Test sonucu aşağıdaki gibidir: Gönderilen test datasında, {10000, 5000,1000}’de haracamdan sonra geriye 5000 yerine 1000 değerinde para kalması gerektiği gönderildiği için hata fırlatılmıştır. Bu örneklerde, özellikle hata alınması amacı ile bazı test dataları yanlış girilmiştir.

TestAccountMultiPayment: Aşağıdaki son test functionında, yeni bir maaş versi ile account oluşturulup, birden fazla harcama yapılmakta, ve geri kalan para miktarı bu toplu harcamadan sonra kontrol edilip test edilmektedir.

  • testGrup : Test amaçlı “salary” yani maaş, “payment [ ]” dizisi yani birden fazla yapılan toplu harcama ve son olarak kalan paranın kontrolünün yapıldığı “state” paramtere dizisi olarak gönderilir.
  • for _, p := range testGrup {” : Herbir test datası tek tek gezilir.
  • a := NewAccount(p.salary)“: Belirlenen test datası miktarında, maaş atanarak yeni bir account yaratılır.
  • var totalPayment = 0“: Toplam yapılan harcamının atanacağı local değişkendir.
    • for _, s := range p.payment {” : Belirlenen test datası içinde tanımlanan toplam harcama dizisi [ ], teker teker maaşdan düşülür.
      • totalPayment += s“: Herbir yapılan harcamanın toplamı, ilgili local değişkene atanır.
      • a.Spending(s)” : Maaşdan düşülen payment [ ] dizisindeki herbir harcama bu function ile yapılır.
      • “if a.State() != p.state {“: Geri kalan paranın, olması gereken para miktarı ile aynı olup olmadığına bakılır. Aynı değil ise hata fırlatılır.
      • “t.Errorf(“Salary %d, Result : %d₺, Expected : %d₺, TotalPayment %d₺”, account.money + totalPayment, account.State(), p.state, totalPayment)”: Yazılan detaylı hata mesajıdır.
        • “(account.money + totalPayment)”: Kalan paraya, toplam harcanan para eklenerek başlangıçtaki maaş miktarı bulunur.
        • “account.State()” : Harcamalardan sonra, maaşdan artık ne geriye kaldı ise bu function ile alınır:)
        • “p.state” : Test datasından gönderilen kalması gereken para miktarı bu değişken ile alınır.
        • “totalPayment”: Yapılan toplam harcamadır.

Yapılan Test sonucu aşağıdaki gibidir: Aşağıda görüldüğü gibi, yapılan testden hata alınmıştır. Gönderilen maaş 10000₺. Yapılan harcamalar(8300₺ + 600₺ + 250₺) dir. Sonunda toplam 9150₺ harcama yapılmış ve geriye test datasından dönülen 3000₺ değerinde bir para kalması beklenmiştir. Ama sonuç 850₺ olduğu için hata alınmıştır. Bu örneklerde, özellikle hata alınması amacı ile bazı test dataları yanlış girilmiştir.

account_test.go(Tüm Kodlar):

Geldik bir makalenin daha sonuna. Bu makalede Go ile test yazmanın ne kadar kolay olduğunu birkez daha gördük. Testler, belki başta zaman alsa da, projenin devamında geliştirme ve değişikliklerde büyük zaman kazandırmakta ayrıca yapılan işlemin doğru olma olasılığını büyük oranda arttırmaktadırlar. Test’in sadece büyük projelerde değil, her tip projede yazılması gerektiğini düşünenlerden biriyim. Ayrıca yazılan bu testleri, örneğin GitHub’a publishden hemen önce, Travis CI veya Jenkins gibi toolar ile çalıştırıp, otomotize eder isek, tadından yenmez. İşte biz buna, CI (Continuous Integration) diyoruz. Testler, bir projenin bussines sürecindeki karşımıza çıkabilecek hemen hemen her ihtimalini üzerinde barındıran, bir çeşit note defterleridir.

Esas önemli olan nokta, doğru Test datası hazırlayabilmektir. Bunun için de, analiz dökümanlarının baştan aşağı detaylıca okunması, tüm kapsamın çıkarılması ve bütün olasılıkları kapsayacak test senaryolarının yazılıp doğru test datası oluşturulması gerekmektedir. Bu yüzdendir ki, doğru data bazen testden daha önemlidir.

Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source:

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

4 Cevaplar

  1. Erhan Yakut dedi ki:

    Bora Hocam güzel bir örnek üzerinden anlatılan harika bir yazı olmuş. Ellerinize sağlık.

  2. Yang dedi ki:

    1. Go’da Interface tanımları I harfi ile başlatılmaz. Aslında hiçbir programlama dilinde I ile başlatılmaması gerekir ancak bu başka bir tartışma konusu.
    2. Bir domain entity’si için arayüz tanımlamak da ne demek bunu hiç anlamadım? Domain entity’sini mock mu ediyoruz?
    3. Son olarak tanımladığınız interface’i nerede kullandık? Herhangi bir yerde kullanıldığını görmedim?

    • borsoft dedi ki:

      Selamlar,

      1-) Go’da interface genel örneklerde “I” ile başlamasa da Java ve C#’dan gelen bir developer olarak “IAccount” isimi ile adlandırmak benim daha kolay aklımda kalmasını sağlıyor. Bence isimlendirme Dilden bağımsız bir olgu. Yani Go’da olmaz C#’da olur kısmı benim mantığıma biraz ters düşüyor.
      2-) Makale içinde “Domain entity”açıklamasını maalesef bulamadım ?
      3-) Interface’i, aslında bir başka IAcount’dan türüyen struct tanımlayıp onu da test içine sokacaktım ama makaleyi daha fazla uzatmamak adına oluşturmadım. Interface de havada kaldı…

      İyi çalışmalar.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.