Test Güdümlü Yazılım Geliştirme – 1
Çevik Yazilim – Agile Development
Test Driven Development – Test Güdümlü Yazilim Gelistirme
Refactor – Yazilimin islev ve davranisini degistirmeden kod yapisi üzerinde yapilan degisiklik.
Yazının ikinci bölümü için tıklayınız…
Çevik süreçlerde kullanilan en önemli yazilim gelistirme metodlarindan biri test güdümlü gelistirmedir. Önce test kodunun yazip sonra islevsel kodu yazdiginiz için, testleri yazdiginiz esnada aslinda ne için ve ne sekilde kod yazacaginizida kafanizda tasarlamis olursunuz.
Çogu gelistirici test güdümlü yazilimin en büyük faydasinin, elinizde ürünle beraber onu test edecek kodunda olmasi olarak görür. Tam olarak uygulandiginda test güdümlü yazilim gelistirmenin bunun çok ötesinde faydalari vardir. Test siniflarini yazmaya basladigininz anda aklinizdaki dizayn bu testlerin etrafinda daha iyiye giderek sekillenir.
Test Güdümlü Yazilim Akis Semasi
Test Güdümlü Yazilim Akis Semasi
1. Kodun basarisiz oldugu testler yazin.
2. Kodu bu testleri geçecek sekilde degistirin.
3. 1. ve 2. adimlari tekrarlayin.
4. Kodu bol bol refactor edin.
5. Akliniza kodu basarisizliga ugratacak baska test gelmiyorsa, basarmissiniz demektir.
Test Güdümlü Gelistirme ve Önce Kod Sonra Test Metodu Karsilastirmasi
Test güdümlü gelistirmede testlerin önce yazilmasi gerekir. Ancak test kodlarinizin yazimi bittiginde veya islevsel kodunuz test kodlarindan geçmeyi basaramadiginda, islevsel kod eklersiniz. Bazi gelistiriciler bu metodu biraz degistirmisler ve önce islevsel kodu yazip sonra test kodunu yazmayi tercih etmislerdir. Sonuç olarak yine elinizde test siniflari olur fakat bu metodla test güdümlü gelistirmenin en büyük avantajlarindan biri olan, test siniflarini yazdikça islevsel kodun nasil olmasi gerektigi bilgisini edinemezsiniz. Yazacaginiz kod aklinizdaki ilk dizayna uygun olur ve bu genelde problemin tam anlasilamadigi, neyin nasil yapilacagina tam karar verilemedigi safhalarda olusturulmus bir dizayndir. Test güdümlü yazilim gelistirmede ise test siniflari yazildikça problem daha iyi anlasilir ve yapilacak dizayn ona göre sekillenir ve mükemmellesir. Daha anlasilir olmasi için bir örnekle devam edecegiz.
Mükemmel Sayilar
Test güdümlü gelistirmenin sagladigi avantajlari görmek için öncelikle çözülmesi gereken bir problem bulalim. Örnekte amacimiz mükemmel sayilari bulmak olacak, matematikten hatirlamayanlar için mükemmel sayilara kisaca deginelim. Bölenlerinin toplami kendisine esit olan sayi mükemmel sayidir.
Mesela 6 sayisi 3,2 ve 1 e bölünür. Bunlarin toplami 3+2+1 = 6 dir.
Mesela 28. 14+7+4+2+1 = 28 dir.
Bu örnekte hem test güdümlü gelistirmeyi uygulamada göstermek hem de “önce kod sonra test metodu” ile karsilastirmak istiyorum. Bu yüzden ilk önce çözümü yapip sonra testlerimi yazacagim arkasindan ayni problemi test güdümlü gelistirme yöntemi ile çözecegim bu sekilde aradaki farki daha net görebiliriz.
Önce Kod, Sonra Test Yöntemi Ile
public class PerfectNumberFinder1 { public static boolean isPerfect(int number) { // get factors List factors = new ArrayList(); factors.add(1); factors.add(number); for (int i = 2; i < number; i++) if (number % i == 0) factors.add(i); // sum factors int sum = 0; for (int n : factors) sum += n; // decide if it's perfect return sum - number == number; } }
Yukaridaki çok muhtesem bir kod olmasada isimizi görmeye yeter. Bütün bölenleri içinde tutabilecegim bir liste olusturdum. Bütün sayilar bire bölünebildigi için onu dogruca bu listeye ekledim. Sonrasindan ikiden baslayarak ta ki mükemmel sayi olup olmadigina baktigimiz sayisa kadar bütün sayilari tek tek, test ettigimiz sayiya böldüm. Eger bölünebiliyorsa, bölen demektir ve bunu listeye ekliyoruz.
Kodu bitirdik ve test koduna geçiyoruz. Biri mükemmel sayilari dogru buluyor mu diye, digeri bulunan sayilar arasina mükemmel olmayanlar karismi mi diye iki tane test yazmam gerekiyor.
Test Sinifi
public class PerfectNumberFinderTest { private static Integer[] PERFECT_NUMS = {6, 28, 496, 8128, 33550336}; @Test public void test_perfection() { for (int i : PERFECT_NUMS) assertTrue(PerfectNumberFinder1.isPerfect(i)); } @Test public void test_non_perfection() { Listexpected = new ArrayList( Arrays.asList(PERFECT_NUMS)); for (int i = 2; i < 100000; i++) { if (expected.contains(i)) assertTrue(PerfectNumberFinder1.isPerfect(i)); else assertFalse(PerfectNumberFinder1.isPerfect(i)); } } @Test public void test_perfection_for_2nd_version() { for (int i : PERFECT_NUMS) assertTrue(PerfectNumberFinder2.isPerfect(i)); } @Test public void test_non_perfection_for_2nd_version() { List expected = new ArrayList(Arrays.asList(PERFECT_NUMS)); for (int i = 2; i < 100000; i++) { if (expected.contains(i)) assertTrue(PerfectNumberFinder2.isPerfect(i)); else assertFalse(PerfectNumberFinder2.isPerfect(i)); } assertTrue(PerfectNumberFinder2.isPerfect(PERFECT_NUMS[4])); } }
Kod mükemmel sayilari buluyor fakat performans sorunlari var. Mükemmel olmayan sayilari kontrol ederken çok fazla harciyor çünkü bir çok sayiyi kontrol ediyoruz. Bölen ve sonucu ayni anda kontrol ederbilirim. Mesela 28 sayisi için 2′yi kontrol ederken ayni anda 14üde kontrol etmis olurum çünkü 28 hem ikiye hemde 14′e bölünür ayni sey 7,4 ikilisi içinde geçerli. Bu sayede sayinin karekökü kadar kontrol yapmam yeterli. 64 sayisi için önceden 64ünü de tek tek kontrol ediyorken artik 8 kontrol yeterli olacaktir.
Gelistirilmis Kod
public class PerfectNumberFinder2 { public static boolean isPerfect(int number) { // get factors List factors = new ArrayList(); factors.add(1); factors.add(number); for (int i = 2; i <= sqrt(number); i++) if (number % i == 0) { factors.add(i); factors.add(number / i); } // sum factors int sum = 0; for (int n : factors) sum += n; // decide if it's perfect return sum - number == number; } }
Fakat bu gelistirmeden sonra kodumuz testlerin ikisinden geçemiyor. Bunun sebebide karekökü tamsayi olan sayilar. Örnegin 16. 16 nin mükemmel soyu olup olmadigini kod test ederken asagidaki sayilari listeye ekleyecek.
1
2,8
4,4
Yukarida da gördügünüz gibi 4ü iki defa eklemis olduk. Aslinda amacimiz bölen ve sonucu ekleyip islemi hizlandirmakti fakat 16/4=4 oldugu için 4 iki kez listeye girmis oldu. Asagidaki degisiklik ile bu hatayi düzeltiyoruz.
for (int i = 2; i <= sqrt(number); i++) if (number % i == 0) { factors.add(i); if (number / i != i) factors.add(number / i); }
Kodu yazdiktan sonra test siniflarini ekledik, hersey düzgün çalisiyor gibi ama dizaynda bazi sorunlar var. Mesela kod bloklarini ayirmak için yorum satirlari ekledim. Bu bloklarin kendi metodlarinin içinde olmasi lazim, yani tek büyük bir metod yerine kodu metodu parçalara ayirmam lazim. En son ekledigim kontrolün yorum satirlariyla açiklanmasi lazim, fakat metodun uzunlugu suandaki en önemli sorunumuz. Java kodu yazarken ki prensiplerimden biri, bir metodun uzunlugunun 10 satiri geçmemesidir. Eger geçiyorsa, o metod birden fazla is yapiyor demektir ve bunlar daha küçük parçalara ayrilmalidir. Bizim kodumuzda da bu problem var.
Test Güdümlü Gelistirme Yöntemi Kullanarak
Test güdümlü gelistirmede sloganimiz, “Testini yazabilecegimiz en basit kod parçasi nedir?” olmalidir. Suanda üstüne çalistigimiz problemde sorumuz, bu sayi mükemmel sayimidir degilmidir sorusu olabilir. Fakat bu problemi tanimlayan çok genis kapsamli bir soru. Problemi küçük parçalara ayirmam gerekiyor.
- Test ettigim sayinin bölenlerine ihtiyacim var mi ?
- Bölenleri toplamaya ihtiyacim var mi ?
- Bir sayinin bölen olup olmadigini kontrol etmeye ihtiyacim var mi ?
Yukaridaki sorulari koda döksek sizin için en kolayi hangisi olur ? Bir sayinin bölen olup olmadiginin testi gayet kolay görünüyor.
Bir sayi baska bir sayinin böleni mi degil mi testi
public class Classifier1Test { @Test public void is_1_a_factor_of_10() { assertTrue(Classifier1.isFactor(1, 10)); } }
Yukaridaki test sinifini derleyip çalistirabilmemiz için, adi Classifier1 olan ve isFactor adinda bir metodu olan test edilecek bir sinifa ihtiyacim var. Öncelikle sinifimizin iskeletini olusturuyoruz. Küçük küçük bir çok test yazdigimizda test edilecek siniflarin yapisi kendiliginden olusmus olur. Bu testleri yazip karsilik gelen sinif ve metodlari olusturdugumda programim en azindan derlenip çalistirilabilir hale gelicek. Bu asamada kodum testlerin hiçbirinden geçemeyecek çünkü sadece kod yapisini olusturduk içine fonksiyonel hiç bir kod yazmadik, yani metodlarimizin içi bos diyebiliriz. Testlerimi kostuk ve sinifimiz testleri geçemedi normal olarak. Simdi sira kodumuz testleri geçene kadar onu modifiye etmekte.
factor metodu ekliyoruz
public class Classifier1 { public static boolean isFactor(int factor, int number) { return number % factor == 0; } }
Isi yapan gayet basit ve öz bir kod oldu. Simdi sira bir sayiyi bölenleri test etmede.
@Test public void factors_for() { int[] expected = new int[] {1}; assertThat(Classifier1.factorsFor(1), is(expected)); }
Yukarida en basiti 1 ve bölenlerinin testini yaptim. Bu testi factorsFor metoduyla yaptik fakat suanda öyle bir metodumuz yok. Ilk is olarak onu olusturacagiz ve bize bir sayinin bölenlerini array olarak döndürecek.
public static int[] factorsFor(int number) { return new int[] {number}; }
Yukaridaki kod dogru degil fakat testi geçmek için yeterli. Buna daha sonra tekrar dönecegiz. Burada isFactor metodunu static yapmak iyi bir fikir gibi duruyor çünkü sadece kendine verilen sayi üzerinde islem yapiyor, sinif içindeki degiskenlerle bir islem yapmiyor. Fakat factorsFor metodumda statik kodumdaki statik metod sayisi fazlalasiyor bu iyi bir sey degil. Bunun için Classifier1 sinifimi biraz degistirip Classifier2 sinifini olusturuyorum.
public class Classifier2 { private int _number; public Classifier2(int number) { _number = number; } public boolean isFactor(int factor) { return _number % factor == 0; } }
Artik number degiskenimi Classifier2 sinifina parametre olarak yolluyorum bu sayede number degiskenini bütün metodlara tek tek parametre olarak yollamaktan kurtuluyorum. Simdi yapmam gereken bir sayinin bölenlerini bulmak. Bunun için asagidaki test sinifini yaziyorum.
@Test public void factors_for_6() { int[] expected = new int[] {1, 2, 3, 6}; Classifier2 c = new Classifier2(6); assertThat(c.getFactors(), is(expected)); }
Simdi bu test metoduna karsilik bir uygulama metodu yazmam lazim. Classifier2 sinifina parametre olarak gönderdigim sayinin bölenlerini array içinde bize döndüren bir metod yazalim.
public int[] getFactors() { List< _number; i++) { if (isFactor(i)) factors.add(i); } int[] intListOfFactors = new int[factors.size()]; int i = 0; for (Integer f : factors) intListOfFactors[i++] = f.intValue(); return intListOfFactors; }
Bu kod testden basariyla geçecektir ama yapi itibariyle hos görünmeyen bir kod ortaya çikti.Test güdümlü gelistirmede uygulama kodunu yazarken bazen bu durumlarla karsilasabilirsiniz. Bunun sebepleri, çok uzun olmasi ve birden fazla isi yapiyor olmasi. Bu metod ileride baska metodlar tarafindan kullanilmak için çok da uygun degil. Bu karmasiklikta bir metod olusturmaniz için, sizin buna mecbur birakan çok geçerli nedenleriniz olmali ve suanda biz bu geçerli nedenlerin hiçbirine sahip degiliz. Belki de “factors” degiskeni getFactors metodunun degilde sinifin degiskeni haline getirirsek bu metodu biraz daha küçük parçalara ayirabiliriz.
Kent Beck’in Smalltalk Best Practice Patterns kitabinda bahsettigi sade (composed) metodlardan kisaca bahsedelim. Bu metodlarin üç temel özelligi vardir
- Programinizi, her biri sadece tek bir isi yapan metodlara bölün
- Bir metodun içindeki bütün islemler ayni soyutlama (abstraction) seviyesinde olsunlar
- Bu sekilde her biri birçak satirlik kisa kisa metodlardan olusan bir program elde edersiniz.
Sade metodlar test güdümlü gelistirmenin tesvik ettigi yöntemlerden biridir. Fakat yukaridaki metodda ben bu yönteme tamamen aykiri hareket ettim. Bunu düzeltmek asagidaki adimlari uygulayacagiz.
- “factors” degiskenini metod degiskeni olmaktan çikarip sinif degiskeni haline getirecegiz.
- “factors” degiskenini baslatma islemini (factors = new ArrayList
- Metodun sonundaki int[] e çevirme islemini simdilik yapmayacagiz.
- “addFactors” metodu için yeni bir test ekleyecegiz.
Dördüncü adim ufak bir is gibi görünsede oldukça önemli. Yukaridaki bozuk kodu yazmam, metodlari parçalara ayirmada da sorunlar yasadigimi gösterdi bana. Yeni ekleyecegim “addFactors” metodu aslinda yukaridaki uzun metodun içinde gömülüydü. Ilk basta bunun farkinda varamadik ama suanda problemin farkindayiz. Bu genelde test güdümlü gelistirme esnasinda olur. Bir test çalistirdiktan sonra, test ettiginiz metodun test edilebilir daha ufak parçalar içerdigini görürsünüz ve onu da daha ufak test edilebilir parçalara bölersiniz. Suanda “getFactors” metodundaki büyük problemi bir kenara birakiyorum ve “addFactors” metoduyla ilgeniyorum.
Add Factors Test Metodu
@Test public void add_factors() { Classifier3 c = new Classifier3(6); c.addFactor(2); c.addFactor(3); assertThat(c.getFactors(), is(Arrays.asList(1, 2, 3, 6))); }
Yukaridaki testin uygulama kodu ise agasidaki gibidir.
public void addFactor(int factor) { _factors.add(factor); }
Büyük bir güvenle testimi kosuyorum fakat, metodum testi geçemiyor. Bukadar basit bir metod testi nasil geçemez ? Sorunu asagida görebiliriz.
Beklenen listem 1, 2, 3, 6 idi fakat bana dönem liste 1, 6, 2, 3. Kodu degistirip 1 ve sayinin kendisini ekleme islemini sinifin yapilandirici metoduna almistim bu yüzden 1 ve 6 önce geldileri. Bu probleme bir çözüm, 1 ve sayinin kendisinin herzaman önce gelecegini göz önüne alip testlerimi ona göre yazmak fakat bu gerçekten bir çözüm mü ? Hayir, problem çok daha temelde yatmakta. Bu sayilari List sinifinda mi tutmaliyim yoksa Set sinifindami. Cevap set sinifi çünkü set siniflarini karsilastiriken 1, 2, 3, 6 ve 1, 6, 2, 3 birbirlerini esittir, fakat ayni seyi List sinifi için söyleyemeyiz.
Set set = new HashSet(); set.add(1); set.add(2); set.add(3); Set set2 = new HashSet(); set2.add(2); set2.add(3); set2.add(1); System.out.println(set.equals(set2));
Yukariadaki kodu çalistirdigimizda true çiktisini aliriz. Bölenleri bir listede tutmak en basta dogru bir çözüm olarak görünmüstü fakat test sonucunda gördükki bu dogru biz çözüm degilmis. List yerine Set kullanarak sadece bu problemi halletmis olmadik ayrica kodda daha iyi bir soyutlama yapmis olduk.
Test güdümlü gelistirme kullanarak mükemmel sayilar ile ilgili bir çalisma yaptik. Önce kod sonra test yöntemiyle de hem dizaynda hemde veri tiplerinde yanlis seçimler yapmistik. Önce kod sonra test ile, programinizi kaba taslak test etmis olursunuz, fakat test güdümlü gelistirme ile en ince noktasina kadar test edersiniz. Ikinci bölümde mükemmel sayilar üzerinde çalismaya devam edecegiz. Test güdümlü gelistirme yöntemiyle gelistirdigimiz kod bittiginde, önce kod sonra test yöntemi kullandigimiz kod ile karsilastirip bazi sonuçlar çikartacagiz. Ikinci bölümde test güdümlü gelistirmeyle ilgili, private metodlari nezaman ve nasil test etmeli, gibi sorulara da cevaplar arayacagiz.
Yazının ikinci bölümü için tıklayınız…
Yazının orjinali için tıklayınız…















Test Güdümlü Yazılım Geliştirme - 2 | engin güller said,
June 15, 2009 at 6:14 am
[...] problem, birinci bölümde önce kod sonra test metoduyla oluşturduğum kod ile aynı. Burada bölenleri ararken 2 den [...]