Ne Zaman Test Güdümlü Yazılım Yapmalıyım?

John Sönmez bu video kaydında ne zaman test güdümlü yazılım yaparım konusuna açıklık getirmeye çalışmış. Özetle test güdümlü yazılımın bir yere kadar mantıklı, birim testlerinin bakımının ve geliştirilmesinin zahmetli bir iş ve oluşan sistem hatalarının birim testleri ile keşfedilmelerinin zor olduğundan bahsetmiş.

Test güdümlü yazılımın sadece birim testleri ile yapılabileceğini düşünüyor. Bu doğru değil! Böyle olduğu için test güdümlü yazılımın arkasında tüm kalbi ve mantığı ile durmuyor.

Kendisinin bahsettiği test güdümlü yazılım tarzı bottom-up tarzıdır. Sistemi oluşturan parçalar (sınıflar) testler aracılığı ile tek tek oluşturulur. Oluşturulmakta olan sınıfların kullandığı diğer sınıflar mevcut değillerse, mock nesneler kullanılır. Böylece uygulama yavaş yavaş temelinden oluşmaya başlar. Bottom-up test güdümlü yazılım birim (unit) testleri oluşturularak yapılır.

Birde topdown namı diğer onay/kabul test güdümlü yazılım (ATDD – Acceptance Test Driven Development) tarzı vardır. Bu tür test güdümlü yazılımda testler son kullanıcı gözü ile uygulamanın en üst katmanından başlayarak, alt katmanlar oluşturacak şekilde geliştirilir. Bu tarz test güdümlü yazılımda birim testleri değil, entegrasyon ya da onay/kabul (acceptance) testleri oluşturulur. Mock kullanan birim testleri oluşturmakta mümkündür. ATDD’de ne zaman hangi tür testlerin kullanıldığını bir örnek üzerinde göstermek istiyorum.

Şimdi bir web uygulaması hayal edelim. Bu uygulama kullanıcıya todo listesini yönetme imkanı sunuyor olsun. Kullanıcı http://localhost/todolist/1 linkine tıkladığında uygulama kullanıcıya daha önce kaydettiği bir todo nesnesini göstermektedir. Böyle bir uygulamayı ATDD ile nasıl geliştirebilirdik? Hemen test yazarak başlayalım.

Web uygulamayı Spring MVC çatısını, testleri Junit ve Spring MVC Test çatısını kullanarak geliştiriyorum. Burada verdiğim örnekler gösterim maksatlı olup, derlenebilir yapıda değildirler. Ama bu örneklerden yola çıkarak, prensipte test güdümlü yazılımın nasıl olması gerektiğini görebiliriz.

Kullanıcının http://localhost/todolist/1 linkine tıklamasıyla oluşan işlemi test etmek için aşağıda yer alan test sınıfını oluşturuyorum.

[sourcecode language=”java”]
//Kod 1
public class TodoControllerTest {

@Inject TodoController controller;

private MockMvc mockMvc;

public void find_todo_by_id(){

mockMvc.perform(get("/todo/{id}", 1L))
.andExpect(status().isOk())
.andExpect(view().name("todo/view"))
.andExpect(forwardedUrl("/WEB-INF/jsp/todo/view.jsp"))
.andExpect(model().attribute("todo", hasProperty("id", is(1L))))
.andExpect(model().attribute("todo", hasProperty("description", is("test todo kaydi"))))
.andExpect(model().attribute("todo", hasProperty("title", is("test"))));
}
}
[/sourcecode]

Bakınız ortada ne TodoController sınıfı, ne view.jsp sayfası ne de Todo sınıfı var. Testin çalışabilmesi için ikinci adımda TodoController sınıfını oluşturmamız gerekiyor. Şimdi bu sınıfı oluşturalım.

[sourcecode language=”java”]
//Kod 2
@Controller
public class TodoController {

private final TodoService service;

@RequestMapping(value = "/todo/{id}", method = RequestMethod.GET)
public String findById(@PathVariable("id") Long id, Model model) throws TodoNotFoundException {
Todo found = service.findById(id);
model.addAttribute("todo", found);
return "todo/view";
}
}
[/sourcecode]

TodoService sınıfını kod 1 de yer alan find_todo_by_id() metodunu tatmin edecek şekilde yapılandırdık. Lakin yine burada ne TodoService sınıfına sahibiz ne de bu sınıfta olması gereken findById() metoduna. Test güdümlü yazılımın bize sağladığı en büyük avantajlardan birisi, oluşturduğumuz testlerin sürekli programlama arayüzünü kullanma (API – Application Programming Interface) isteği ve eğiliminde olmalarıdır. TodoController sınıfında yer alan findById() metoduna baktığımızda service.findbyId() isminde, servis katmanında yer alan TodoService sınıfının findById() isimli metodunu kullandığını görmekteyiz. Böyle bir sınıf ve metot henüz yok, ama böyle bir yapıya ihtiyacımız olduğunu kod 1 de ki test aracılığı ile anlamış durumdayız. Testler uygulamayı oluştururken uygulamanın dışa sunduğu API’yi de inşa etmektedirler.

Kod 1 de yazdığımız test bizi doğrudan TodoController sınıfına, oradan TodoService, oradan da findById() metoduna yönlendirdi. Bu perspektiften bakıldığında testler oluşturmamız gereken API’nin parçalarını çok çabuk keşfetmemizi ve sadece gerekli sınıf ve metotları oluşturmamızı mümkün kılmaktadır. Test güdümlü yazılım yapıldığında sadece ve sadece gerekli olan sınıf ve metotlar oluşur. Test güdümlü çalışılmadığı taktirde bunun tersi oluşur ve programcı “belki bu metotta/sınıfta bir işime yarar” diye birçok işe yaramayan ya da kısa bir zaman sonra geçerliliğini yitiren, ama yine de var kalan metotlar/sınıflar oluşturur. Kullanılmayan bu sınıf ve metotları testler olmadan keşfetmek imkansızdır.

Kod 1 de yer alan testin ne tür bir test olduğunu düşünüyorsunuz?TodoControllerTest.find_todo_by_id() bir birim testi midir, yoksa bir entegrasyon testi midir? Yoksa onay/kabul testi mi olduğunu düşünüyorsunuz. Ben de bilmiyorum! Buna bir sonra atacağımız adım karar verecek.

Birlikte görelim.

Bir yol ayrımına gelmiş durumdayız. Gideceğimiz yön, oluşturduğumuz testin türünü tayin edecektir. Eğer TodoController sınıfında yer alan TodoService sınıfı için bir mock nesne kullanırsak, kod 1 de yer alan test metodumuz bir birim testi haline gelir. Eğer TodoService interface sınıfını implemente edip, veriyi bir DAO sınıfı üzerinden veri tabanından alacak şekilde yapılandırırsak, o zaman kod 1 de yer alan test bir entegrasyon ya da onay/kabul testi haline gelecektir.

Kod 1 de yer alan test metodu Spring MVC Test çatısını kullanıyor. Spring MVC uygulamalarını Tomcat gibi bir uygulama sunucu olmadan test etmek mümkün. Bu amaçla MockMvc sınıfını kullanabiliriz. Bu sınıf web uygulaması Tomcat içinde çalışır durumdaymış gibi davranarak, controller sınıflarını test etmemizi mümkün kılmaktadır. Testimizden yola çıkarak controller sınıfına kadar indikten sonra, oluşturduğumuz testin birim, entegrasyon ya da onay/kabul testi olduğuna kendimiz karar verebiliriz.

Özellikle web uygulamalarında ATDD yapabilmek için uygulamanın uygulama sunucusu dışında çalışır durumda olması gerekmektedir. Spring MVC Test gibi çatılar bunu mümkün kılmaktadır.

Ben kod 1 de yer alan testin bir entegrasyon testi olmasına karar verdim. Bu durumda TodoController sınıfında yer alan TodoService sınıfını TodoDao sınıfını kullanacak şekilde implemente etmem gerekiyor. TodoDao sınıfı bünyesinde verilere JDBC ya da Hibernate ile doğrudan erişebilirim. Bu amaçla test ortamında veri tabanını çalıştıracak, gerekli verilerle yükleyecek, benim için entegre bir sistemin oluşmasını sağlayacak ve böylece testin çalışabileceği ortamı oluşturacak araçlara ihtiyaç duymaktayım. Örneğin testleri yazarken H2 gibi bir veri tabanı sistemi kullanabilirim. Bu veri tabanı sistemini test koşmadan önce @Before ile işaretlenmiş bir metot bünyesinde çalıştırabilir, gerekli verileri DBUnit ile veri tabanına yükleyebilir ve akabinde testlerimi koşturabilirim. Bu tam bir entegre sistem olduğu için, kod 1 de oluşturduğum test bir entegrasyon testi haline gelmiştir.

Topdown test güdümlü yazılım yaparken en tepeden sisteme girerek, testlerim eşliğinde en alt katmana kadar inmekteyim. Böylece uygulama adım, adım yukarından aşağıya doğru oluşmaktadır. Ben web uygulamalarımı genelde bu şekilde geliştiriyorum.

John’a bir yerde hak vermek lazım. Buttom-up türü test güdümlü yazılımda çok fazla mock kullanımı oluyor ve birçok birim testi oluşabiliyor. Çoğu zaman birim testleri ile oluşan hataları keşfetmek mümkün olmayabilir. Birim testleri her zaman doğruyu söylemezler. Buna karşın entegrasyon testleri de yalan söylemezler. Bu karmaşada istikameti kaybetmemek için bottom-up yerine topdown test güdümlü yazılım kullanılabilir.

TDD karşıtı olan programcalardan duyacağınız argümanları bir önceki paragrafta sıraladım. Bu tür programcılar genelde bir süre bottom-up tarzı çalıştıktan sonra, bekledikleri verimi alamadıkları için TDD’nin evrensel geçerliliğe sahip olmadığını savunurlar. Lakin madalyonun diğer yüzünü görememektedirler. Her türlü uygulamayı bottom-up ya da topdown yönetimi ile geliştirmek mümkündür.


EOF (End Of Fun)
Özcan Acar


Yorumlar

“Ne Zaman Test Güdümlü Yazılım Yapmalıyım?” için 4 yanıt

  1. engin avatarı
    engin

    Tdd yapısına yeni yeni başlıyorum. Hangisine göre sistemi yazmak dha mantıklı ?

    1. Özcan Acar avatarı
      Özcan Acar

      Web tabanli bir uygulama ise, o zaman topdown tarzi daha uygun olacaktir. ATDD yaparken sistemin bazi bölümlerini de bottom-up tarzi gelistirebilirsiniz, yani iki türü de kombine etmek mümkün.

  2. Batuhan avatarı
    Batuhan

    Peki top-down yaklaşımını controller’dan itibaren kullandığınız durumlarda service katmanı testlerini de integration testleri şeklinde yapıp gerçek bir db’ye mi sorgu atıyorsunuz yoksa onları yazarken dao’yu mock’layıp izole bir şekilde mi test ediyorsunuz? Eğer service katmanını da controller’ları test eder gibi test edersek bu durumda hem controller hem de service testlerinin @Before ilkyüklemelerini çiftlemiş olmaz mıyız? Neticede ikisinde de öncesinde db’ye veri yüklemeleri yapılacak. Belki de bu durumda ‘default’ test ilkyüklemesi oluşturup ayrı bir sınıftan okumak lazım entity’i.

    1. Özcan Acar avatarı
      Özcan Acar

      Topdown TDD yaparken bazen gercek veri tabanini kullaniyorum, bu durumda entegrasyon testleri yazmis oluyorum, bazen de alt katmanlari mocklayarak gercek birim testleri yaziyorum. Bu benim nasil ilerlemek istedigimle ve projenin kapsami ile ilgili bir durum.

      Birde extreme topdown tdd türü olarak isimlendirdigim bir TDD türü daha uyguluyorum. Buna göre TDD ile olusturdugum testlerin hepsi onay/kabul testleri. Bu testleri implemente ederken kesinlikle mock kullanmiyorum. Testler en tepeden sisteme girip, en altindan cikana kadar tüm gerekli modüllerin olusmasini sagliyor. Buna gösterim, servis ve veri katmaninda yer alan siniflar da dahil. Buna göre onay/kabul testleri gösterim katmanini sekillendirecek sekilde yapilaniyor. Gerekli tüm katmanlar olusmadigi sürece test kirmizi kaliyor. Taki en altta yer alan veri tabani tablosuna erisip, gerekli islemleri veri katmani üzerinden yapana kadar testleri ve paralelinde gercek isletme mantigi kodunu gelistirmeye devam ediyorum. Test yesil oldugunda bir onay kabul testi icin gerekli her seyi implemente etmis oluyorum ve sistem calisir hale oluyor. Bunun üzerinde kullanici hikayesinde (user story) yer alan diger onay/kabul testlerini implemente etmeye devam ediyorum. Tüm onay/kabul testleri implemente edildiginde, kullanici hikayesinin implementasyonunu tamamlamis oluyorum.

      Extreme topdown yaparken H2 gibi in memory calisan veri tabani sistemlerinin kullanilmasinda fayda var. Bu olusan entegrasyon testlerinin hizli calismalarini sagliyor. Bu sekilde cok genis kapsamli refactoring yapma yetenegim bulunuyor, cünkü entegrasyon testlerinin tamamlanmasi saatleri bulabilir. Gercek anlamda refactoring yapabilmek icin TDD ile birim testlerinin olusturulmasi gerekir.