Bugün birim test(unit test) ve entegrasyon testleri(integration test) yazıyorsanız hayatınıza renk katacağını inandığım bir konuyu kaleme alacağım. Junit4’te var olan ancak az bilinen @Rule ve @ClassRule annotationlarının ne olduklarını ve bunlarla neler yapabileceğimizi örneklerle incelemeye başlayalım.
1. Giriş
“Tüm yazılım geliştiricilerin bilmesi gereken temel prensipler vardır, bunlara KISS, DRY, YAGNI, vb örnek verilebilir. Ama gözden kaçan bir nokta, bunların sadece business taşıyan kodlar için geçerli olduğu düşünülür. Ya test kodları? Örneğin Don’t Repeat Yourself(DRY) gerçekten de test kodları için de önemli bir konu değil midir?”
Rule kavramını ilk duyduğunuzda aklınıza ne geldi? Muhtemelen kelime anlamından testlerin hangi şartlar altında çalışacağının tanımlandığı kuralları barındıran bir yapı olduğunu aklınızdan geçirmişsinizdir. Eğer öyle ise bingo, yaklaşımınız doğru!
Ama aklınıza muhtemel gelen başka soru işaretleri de vardır. Örneğin;
“Ben zaten bu tarz ihtiyaçlarımı zaten @Before ve @BeforeClass annotationlarını kullanarak da karşılayabiliyordum, niçin böyle bir yapı mevcut?”
sorusu aklınızdan geçiyor olabilir. Veya
“Ben zaten kurallarımı bir test metodumun içerisinde kurgulayarak zaten testimi kodluyorum, niçin metod dışına çekme ihtiyacım olsun ki?”
gibi sorulara sahipseniz doğru yoldayız. Öncelikle JUnit içerisindeki halihazırda tanımlı kuralları inceleyerek başlayıp, sonrasında custom rule’lar ile neler yapabileceğimizi inceleyeceğiz. Tekrar eden birçok ihtiyacı merkezi şekilde çözmenin yollarını göreceğiz. Eğer rule’suz hali mümkün ise nasıl yapılabileceğini de örnek olarak verip kazanımı gözlemlemeye çalışalım.
NOT: Rule’unuzu @Rule annotation’ı ile metodlara, @ClassRule annotation’ı ile sınıflara bağlayabilirsiniz.
2. Kurallar (Rules)
2.1. Timeout
Bildiğiniz gibi yazdığımız testlere maksimum çalışacakları süre mutlaka vermemiz gereken bir bilgi. Çünkü sadece çalışan testin veya test double’ının uzun sürmesi halinde bir şeyler yolunda gitmiyor olabilir, bu da atlanılmaması gereken bir konudur.
Ufak bir parantezle Test Double kavramını biraz açalım. Genel bir kavram olmakla beraber gerçek ortamda çalışacak objelerimizin yerine geçecek tüm yapılandırmalarımızı kapsamaktadır. Adından da anlaşıldığı üzere iki parçadan oluşur:
- SUT (System Under Test, yani test edilen kod parçası)
- DOC (Dependend On Component, yani test edilen kod parçasının bağımlı olduğu kaynaklar)
Bu bilgiden sonra timeout ihtiyacını rule kullanmadan karşılamak istediğimizde aşağıdaki gibi tüm metodların annotationlarına bu bilgiyi vermemiz gerekli.
Eğer yukarıdaki gibi her metodun üzerinde tek tek vermek istemiyorsanız ve sadece milisaniye cinsinden süre yönetmemeyi tercih ediyorsanız aşağıdaki şekilde bir kullanımla elinizi hafifletebilirsiniz.
2.2. TemporaryFolder
Eğer file i/o testleriniz varsa geçici dosyalar yaratmanız, bunları test sonrasında temizlemeniz sizin için hayattan bezdirici olabilir. Özellikle de iç içe folderlara ihtiyacınız varsa işler çığrından çıkabilir. Buradaki tüm süreci kendiniz yönetecekseniz en basit haliyle muhtemelen şu şekilde bir kod yazmanız gerekli.
Eğer bu süreçlerle hiç uğraşmayayım, hızlıca asıl odaklanmam gereken kodumu yazayım, işim bittiğinde de kendisi temizlensin derseniz TemporaryFolder ruleunu kullanabilirsiniz. Windows için folder AppData altında yaratılıp testler bitince siliniyor. Dilerseniz bu konumu rule’un initiation’ı esnasında constructor’a vererek konfigure edebilirsiniz.
2.3. RuleChain
Her bir testinizi çevreleyen rulelar dizisi oluşturmak isterseniz RuleChain tam size göre! Kullanımını aşağıda görebilirsiniz. (bağımlı olunan LoggingRule sınıfına custom rule örneğinden erişebilirsiniz.)
2.4 Watchman (TestWatcher)
Çalışan testlerinizin lifecycle’ındaki her bir statü durumunda aksiyonlar almanızı sağlayabilecek yetenekli bir rule. Testlerinizin çalışma durumunu raporlayabilir, adımları custom kodlarınızla zenginleştirebilirsiniz.
2.5. ExpectedException
Bir test metodunda exception senaryosunu test etmek istediğinizde birçok farklı yol mevcut. İyi bir opsiyon olmasa da try-catch bloğu ile kontrollerinizi yapabileceğiniz gibi AssertJ gibi kütüphaneler ile daha komplike exception kontrollerini kolayca yapabilirsiniz. Ancak bir kütüphane kullanmadığımızı varsayarsak en optimum exception kontrolü aşağıdakine benzer bir yapıda olacaktır.
Yukarıdakine alternatif olarak bunu ExpectedException kuralı ile aşağıdaki şekilde yapabiliriz. Bu örnekte herhangi bir utility kullanmadan exception mesajlarını da test edebildik. Dilerseniz Hamcrast Matchers sınıflarını da expect metoduyla kullanabilirsiniz. Burada bir detay, exception alındıktan sonra metodun çalıştırılmasına devam edilmeyecektir. Bu sebeple aşağıdaki örnek metodların sonlarındaki fail satırları çalışmayacak, testler başarılı sonlanacaktır.
2.6 TestName
Bu rule @TestRule rule’unun biraz özelleşmiş hali. Çalışmakta olan testin metodunun adını test çalışırken almanızı sağlıyor. Farklı kullanım alanları sizin hayal gücünüze kalmış.
2.7. Custom Rule - LoggingRule
Aslında hazır kurallar yukarıdakilerden ibaret değil, diğer Rule’lara JUnit takımının wikisinden erişebilirsiniz. Farkındaysanız Rule’lar tekrarlayan ihtiyaçları test metodu veya test sınıfı seviyesinde izole etmek için güzel bir yöntem. Buradan itibaren birkaç tane kompleks kullanım senaryolarına örnek vereceğim.
Custom bir rule yazmak için ihtiyacımız TestRule interface’ini ve onun apply metodunu implement etmek. İhtiyaç duyduğunuz argümanları da constructor’dan toplayabilirsiniz. Testin state’ine ve açıklamasına ilişkin detaylar apply metodunun argümanları olarak elimize gelecek. Örneğin RuleChain örneğindeki loglama yapan bir rule gerçekleştirimi şu şekilde yapabiliriz.
2.8. Custom Rule - MockInitRule
Başka bir örnek olarak mocklarınızın initiation işlemini ortaklaştıran bir rule yazabilirsiniz. Örneğin bir servisiniz ve bu servisinizin bağımlı olduğu başka bir servisiniz olsun.
Bu servisi mocklayarak testi yapan sınıfımız da aşağıdaki gibi olsun.
Burada @Mock annotationı ile işaretlenen servisin instance’ının bağlanması için Mockito’nun bu sınıfta initiation’ı başlatması gerekli. Ayrıca sonrasında mocklanan servislerin davranışlarının tanımlanması da gerekir. Doğruyu söylemek gerekirse bu işlem birçok test sınıfında tekrarlanan bir işlemdir. Bunun için bir rule yazabilir, bu rule aracılığı ile mockların bağlanmasını sağlayabiliriz.
Ayrıca tekrar kullanılabilir bir davranış seti oluşturmak istiyorsak da yukarıdaki MockInitRule sınıfını extend eden bir sınıf yazabiliriz. Constructorda davranış setini verdiğimiz taktirde artık tekrar tekrar kullanabileceğimiz bir mock setini hazırlamış oluruz.
2.9. ExternalResource
Son örnek olarak ExternalResource kavramından bahsedelim. Örneğin testlerinizin öncesinde ayağa kaldırmanız gereken bir sunucu var, bir resource var. Bu resource sizin testleriniz kapsamında otomatize edebildiğiniz bir şey değil. İşte bu noktada ExternalResource ile bir rule tanımlayıp ihtiyaçlarınızı bu yol ile halledebilirsiniz. Örnek kodumuz;
Bunu kullanan testimiz;
Ve son olarak çıktımız;
3. SONUÇ
Yukarıdaki örneklerde de anlattığımız gibi, @Rule ve @ClassRule annotationlarının geniş kullanım alanları mevcut. Tekrar kullanılabilir test yazmak, test sınırlarını genişletmek, yazılmış testlerin statelerini yakından takip edebilmek ve mevcut testlerle yapamadığımız dış kaynakların başlatılması ve sonlandırılması gibi alanlarda kullanılabileceği gibi tamamen custom rule’lar yazarak yapabileceklerimizin sınırlarını ortadan da kaldırabiliriz.