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.