Test Yazarken Uyulması Gereken 7 Kural

En sonunda bu da oldu ve bende “yapılması gereken x ÅŸey” tadındaki baÅŸlığımı attım. Bakalım söylendiÄŸi kadar hit getirecek mi. “Hit” kaygısıyla atılmış olsa da benim için çok iddialı bir baÅŸlık oldu, ama test yazma konusunda o kadar iddialı deÄŸilim. Her gün yeni biÅŸeyler öğreniyorum. Bu yazı da ise bu zamana kadar kendi hatalarımdan çıkardığım 7 dersi, kurallar halinde paylaÅŸacağım. En azından bir kaçı iÅŸinize yarayacaktır diye düşünüyorum.

“Test”‘den kastım tabi ki birim testler, ama buradaki kurallarının benzerlerinin diÄŸer test türleri için de geçerli olacağını düşünüyorum. Yazının tamamında, örnek vermem gerektiÄŸinde JUnit, Mockito ve Hamcrest araçlarını kullanacağım. Ön bilgilendirmeyi yaptığıma göre kurallarıma geçebiliriz.

1. Test İçerikleri Belirli Bir Yapıda Hazırlanmalı

Yazılımsal ya da mimarisel bir kural değil bu. Çok daha basit. Birim testinizi yazarken tutarlı olun ve belirli bir yapıda yazın. Yani biri ki sınıf oluşturup assert yapıp sonra bir iki metod çağırıp peşinden tekrar assert yapmayın. Belirli bir yapınız, belli bir düzeniniz olsun.  Bunun nasıl bir düzen olacağını takımınızın dışındaki birinin söylemesi zor. Ben elimden geldiğince Given-When-Then yapısını takip ediyorum. Bu yapı hem testlerinizi oluştururken hem de daha sonrasında okurken büyük kolaylık sağlıyor.

Given-When-Then

Çok basit bir yapı. Temel olarak testlerinizi 3 ana bölüme ayırıyorsunuz. BildiÄŸim kadarıyla BDD‘de sık kullanılan bir yapı. Normal birim testlerinizi yazarken kullanmamanız için hiç bir sebep yok tabi kide.

  • Given: Bu bölümde, test altında olan sisteminizin (SUT, System Under Test) durumunu ayarlıyorsunuz. EÄŸer Mockito kullanıyorsanız, mockladığınız nesnelerden beklentileriniz burada ayarlıyorsunuz. EÄŸer stublarınızı oluÅŸturacaksanız burada oluÅŸturabilirsiniz. Bu bölümde yaptığınızla @Before aÅŸamasında yaptıklarınız karıştırmayın ama. Sadece parametreleri ve beklentileri ayarlıyorsunuz.
  • When: Test altındaki sisteminizi burada tetikliyorsunuz. EÄŸer düzgün bir birim test yazıyorsanız, bu bölüm bir ya da iki satırı geçmeyecektir. EÄŸer birden fazla metodu tetikliyorsanız, yazdığınız birim test olmayabilir.
  • Then: Her türlü assert ve verify iÅŸleminizi burada yapıyorsunuz. (Assert ve verify için de kurallarımız olacak tabii)

Bu yapıları ise yorum satırıyla ayırabilirsiniz. İnternette bakacak olursanız, ‘label‘ kullananlar da var.  Bir de örnek vereyim ki daha açıklayıcı olsun.

Eğer yapı hakkında daha detaylı bilgi almak istiyorsanız aşağıdaki makalelere güzel bir başlangıç olabilir.

2. Exception’lar Düzgün Assert Edilmeli

Bir uygulamanın en önemli parçalarından biri Exception’ların nasıl handle edildiÄŸidir. Fakat gelin görün ki çoÄŸu zaman exception durumları küçümsenir. Ya hiç test edilmezler ya da test edilseler bile kötü bir ÅŸekilde assert edilirler. Exception’ları düzgün assert etmek için öncelikle düzgün exception’ın nasıl olduÄŸunu anlamak gerekiyor. Gelin çoktan seçmeli bir test yapalım.

Veritabanı iÅŸlemi yapan sınıflarınız birinde ‘findUserById’ isimli bir methodunuz var. Parametre olarak userId alıyor ve siz bunu validate edip IllegalArgumentException fırlatacaksınız. Bu senaryo için aÅŸağıdaki exception’lardan hangisi uygundur?

  1. throw new IllegalArgumentException();
  2. throw new IllegalArgumentException("Validation Failed!");
  3. throw new IllegalArgumentException("User Id is negative!");
  4. throw new IllegalArgumentException(String.format("User Id [%d] is negative! User Id must be positive in order to search!", userId));

Büyük ihtimal zaten anladınız ama Cevap D. C bir yere kadar kabul edilebilir. ‘Negative’ ifadesi bu örnek için anlam ifade ediyor. Ama daha detay gereken durumlarda hataya yol açan parametreleri exception’da belirtmenizde fayda var.  Tabi ki düzgün exception oluÅŸturmak bu kadarla bitmiyor. Bunun yanı sıra iç içe geçen exceptionlarda sebep (cause) bilgisini kaybetmemek gerekiyor. Daha fazlası için Effective Java’yı okumanızı tavsiye ederim.

Bir parça düzgün Exception’ın nasıl olduÄŸunu anladığımıza göre ÅŸimdi bunları nasıl assert edeceÄŸimize bakalım.

Öncelikle klasik exception assert etmenin bizi kurtarmayacağınızı anladığınızı düşünüyorum.

EÄŸer aklınıza try…catch yapmak geldiyse, hemen unutun. (Oldu olacak iki de if…else yazıp, canlıya alın kodu!)

Bizim yapacağımız ‘Rule‘ kullanmak olacak. Ek olarak, exception test ederken de farklı bir yapı kullanmamız gerekiyor. Ne yazıkki 1. Kuralda anlattığım Given-When-Then yapısını doÄŸruca kullanamıyoruz. Bu tamamen exception’ların yapısından kaynaklanıyor. Ben de biraz devÅŸirip Given-Then-When olarak kullanıyorum. Yine aynı 3 bölüm sadece yerleri deÄŸiÅŸik. Testimiz deki örneÄŸi bu doÄŸrultuda gerçekleyecek olursak.

3. Yardımcı Kütüphaneleriniz Düzgün Kullanılmalı

Başka bir deyişle, test sonuçlarınızı düzgün assert edin. Beklentilerinizi mutlaka verify edin.

Test kütüphanesinden kastettiÄŸim Mockito ve Hamcrest… Test yazarken benim için olmazsa olmaz iki kütüphane. Hem test etmemi çok kolaylaÅŸtırıyorlar hem de okunabilirliÄŸi arttırıyorlar. İki taÅŸla koca sürüyü indiriyorsunuz öyle düşünün. Bir çok kiÅŸi de benim gibi düşünüyor olacak ki bir hayli kullanılmaktalar. Fakat kullanmaya baÅŸlamanın kolay, ustalaÅŸmanın zaman aldığı iki kütüphane.

Mesela Hamcrest, hala öyle assertionlar görüyorum ki beni hayrete düşürüyor. Adam bir assertThat ifadesiyle, koca objeyi tek serferde, bin tane matcher kullanarak assert ediyor. Hamcrest’in bunu yapabilmesi çok güzel tamam, ama bu doÄŸru olduÄŸu anlamına gelmiyor.  Testlerinizin okunabilirliÄŸi gittiÄŸi zaman hiç bir anlamı kalmıyor. EÄŸer sizden sonra gelecek kiÅŸi o kod bloÄŸunun ne yaptığını ilk görüşte anlamıyorsa mümkün olduÄŸunca uzun ifadeler yazmaktan kaçının. Kısa ve anlaşılır ifadeler kullanın.

Az önce söylediÄŸimle çeliÅŸiyor gibi gelebilir ama deÄŸil. EÄŸer hamcrest kullanıyorsanız, mümkün olduÄŸunca tüm assert’lerinizi hamcrest ile yapın. Ne kadar basit olursa olsun. Kısa hamcrest ifadeleri hem daha anlaşılır oluyor hem de kod yapısı olarak tek düze oluduÄŸundan okunabilirlilik artıyor.

Biraz da Mockito’dan konuÅŸalım. Mockito kullanırken yapabileceÄŸiniz en büyük hata, verify etmemek olur. Tüm beklentilerinizi mutlaka Then aÅŸamasında verify edin. Hatta, eÄŸer hiç deÄŸinmediÄŸiniz mock’larınız da varsa onları da verify edin (verifyZeroInteractions). Mockito kullanımında diÄŸer en çok gördüğüm hata ise void metodları test ederken yaÅŸanıyor.

Mesela aşağıdaki testte, void bir metoda gönderilen parametre test edilmek istenmiş. Hani doğru parametre ile çağırılıyor mu gibi bir test.

Öncelikle GivenWhenThen yapısına uyulmamış. Sonra, mockito çok kötü bir şekilde kullanılmış.  Test kesinlikle okunabilir değil. Bunun tek sebebi kullanılan kütüphanenin çok bilinmemesi. Halbuki, bunu mockito kullanarak çok daha kolay yapabilirsiniz.

4. Testler Düzgün İsimlendirilmeli

Aslında, testlerin düzgün şekilde dökümante edilmesi falan da gerekiyor ama asıl kodun dökümante edilmediği bir yer de testlerin dölümante edilmesini istemek gerçekçi değil. Ama en azından testlerin isimlendirilmesi düzgün yapılmalı. Testlerin isimleri test ettikleri durumu anlatmalı.

Mesela gelin bu yazı boyunca oluşturduğumuz testlere ve isimlerine bir göz atalım.

  1. cafeShouldNeverServeCoffeeItDoesntHave : Test ettiğimiz durum, test ettiğimiz sistem herşey belli.
  2. testFindUserByIdShouldThrowExceptionWhenUserIdIsNegative : Yine aynı ÅŸekilde. Fakat 1. şıkka göre daha detaylı anlatılmış durum. Bana testin structure’ı hakkında da bilgi veriyor. 1. seçeneÄŸin az sözle daha fazla ÅŸey anlattığı da bir gerçek.
  3. testPublishSouldSendAddress : Heralde yazı boyunca yaptığımız en kötü isimlendirme. Yine de  testPublish  gibi genellikle rastladığımız isimlendirmelere nazaran daha iyi.

Benim için bu kadarı yeterli, fakat durumu daha abartanlar da gördüm. Örneğin checkstyle üzerinden insanları belli formatta yazmaya zorlayabiliyorlar. Mesela test_{methodName}_WHEN_{sth}_SHOULD_{sth} gibi, kalıpları belirleyip, checkstyle üzerinden bu kalıpları kontrol ediyorlar. Bana fazla geliyor. Özellikle code review sürecinden geçiyorsa zaten ikinci bir göz metod isimlerinin anlaşılır olduğunu denetlemiş olacak.

5. Gereksiz Assert’lerden Kaçınılmalı

Çok temel bir konu ama çok sık yapılan bir hata. Özellikle Hamcrest kullanılmayan projeler de Collection dönen testlerde yapılıyor.

Mesela UserService sınıfımızda getAllUsers diye bir metodumuz olsun ve bu da Liste halinde UserDao’dan okuduÄŸu User’ları dönsun. Bunun testi yaklaşık olarak aÅŸağıdaki gibi olacaktır.

Burada 3 tane assert gereksiz. Tek bir listede 3 tane fazladan assert yapılıyorsa, daha karmaşık testlerde siz düşünün. Olabildiğince gereksiz yapılan assertlerden kaçının. Böylelikle hem testlerinizin okunabilirliğini arttırırsınız hem de bakımını kolaylaştırırsınız.

6. Reusability Kavramının Testlerde Farklı Olduğu Unutulmamalı

Testler de Reusability denildiğinde çoğu kişinin ilk yöneldiği kalıtım (inheritance) oluyor.  Testler için hiç uygun bir yöntem değildir.  Öncelikle test kodunuzu farklı sınıflara dağıttığınızdan okunabilirliği kötüleştiriyorsunuz. Sonra, testlerinizin başarısını ana sınıfınızın merhametine bırakıyorsunuz. Ana sınıfınızda bir hata olduğunda tüm testleriniz fail etmeye başlıyor.

Peki çözüm ne? Hiç mi reusability’e dikkat edilmeyecek? Tabii ki hayır. Birim testte yazsanız hala kod yazıyorsunuz ve belli bir standartta olması gerekiyor. Hala DRY, SOLID, KISS gibi prensiplere uymalısınız. Sadece testlerde kodunuzu farklı ÅŸekilde tekrar kullanılabilir yapacaksınız. Neleri kalıtım üzerinden tekrar kullanmaya çalıştığımıza bakalım:

  • Before-After methodlarını tekrar kullanmak için: Testinizi ayaÄŸa kaldırırken belli iÅŸlemler yapmanız gerekiyor ve siz bunun için kalıtımı kullanmaya karar veriyorsunuz. Fakat gelin görün ki doÄŸru bir karar deÄŸil. EÄŸer Before-After metodlarınızı tekrar kullanmak istiyorsanız, Rule yazın. Mesela bir projemde, CXF servislerimi testleri için memory’de CXF’i ayaÄŸa kaldırmam gerekiyordu. Bunun için bu kuralı yazdım ve Before-After methodlarınden kurtulduÄŸum gibi kalıtım da yapmama gerek kalmadı. Zaten kullandığınız frameworklere bakacak olursanız da benzer bir yol izlediklerini, (kendi annotasyonlarını ve runner’larını yazdıklarını) göreceksiniz. En zahmetsiz olanı Before-After’larınızı Rule haline getirmek.
  • Aynı assert iÅŸlemlerini tekrar tekrar yazmamak için: Testlerinizi yazarken öyle bir an geliyor ki hep aynı ÅŸeyleri assert ettiÄŸinizi hissediyorsunuz. Bir iki string ifadesi ya da deÄŸeri deÄŸiÅŸtiriyorsunuz sadece. Çok can sıkıcı olabiliyor. Bu gibi durumlarda hemen Abstract class’a yönelip bir method yazası geliyor insanın. Ama siz siz olun yapmayın. Onun yerine oturun Hamcrest üzerinden kendi Matcher’ınızı yazın. Emin olun çok daha resusable olduÄŸunu göreceksiniz.
  • Karmaşık nesne yapılarını yaratmak için: Bazı uygulamaların nesne yapıları çok karmaşık olabiliyor. Bunları her test sırasında tekrar tekrar yaratmak ise çok sıkıcı oluyor. Bu sebepten Abstract class’a bir method atıp iÅŸinizi kolaylaÅŸtırmak isteyebilirsiniz. Ama yine bunu yapmanızı tavsiye etmiyorum. Benim yaptığım, her model class’ı için Stub sınıfları yaratmak. Stub sınıfları benim için istersem rastgele oluÅŸturulmuÅŸ olarak istersem benim verdiÄŸim deÄŸerler üzerinden nesnelerimi oluÅŸturuyorlar. Daha sonra bunları kütüphane halinde proje ile birlikte dağıtıyorum ki üst modüllerde baÅŸka biri de aynı nesneyi yaratmak istediÄŸinde zorlanmasın. BaÅŸta stub’ları yazarken çok sıkılıyor insan ama proje ilerlediÄŸinde çok rahat ediyor.

7. Testlerinizin Tasarımınızla İlgili İpuçları Verdiği Unutulmamalı

Son kuralım tasarım ve test iliÅŸikisi üzerine. EÄŸer testiniz sırasında fazla iÅŸlem yapıyorsanız, fazla assert, fazla mock yapıyorsanız, tasarımınızla ilgili bir sorununuz olabilir. Birim testlerinizin tek bir öğeyi, tek bir akışı test etmesi gerekiyor. EÄŸer kodunuz bunu yapmanıza izin vermiyorsa, bir yerlerde (büyük ihtimal mimarinizde ve tasarımınızda) bir sorun vardır. Bunu en çok “Proje çok karmaşık, unit test yazamıyoruz” ÅŸeklinde duyuyorum. Türkçesi, “biz projeyi tasarlayamadık, mimari falan hep çöp” demek.  Genel de bu ifadeyi “Neden unit test yazmıyorsunuz” diye sorulduÄŸunda alırsınız.

Bunu en iyi anlamanızın yolu, test sırasında kontrol edemediğiniz bir yan etkinin olup olmadığına bakmaktır. Test altındaki sisteminiz, test sırasında başka bir servisi kullanıyorsa ve siz o servis durumunu (mesela mockito kullanarak) ayarlayamıyorsanız, bilin ki tasarımınızla ilgili bir sorun var.