Bundan önceki yazılarımda bazı yerlerde, yazılan programların testlerinden çok üstün körü de olsa bahsetmiştim. Şimdi ise bu ihtiyacın neden ortaya çıktığından ve nasıl bir yöntemle yapılabileceğinden bahsetmeye çalışacağım.
Her yazılımcının eninde sonunda yazdığı kodları bir şekilde test ediyor olması gerekir. Ancak bu şekilde yaptığı işin gerçekten çalışıp çalışmadığından haberdar olabilir. Daha sonra yazdığı kodlar bir ürüne dönüştüğünde de bunlara hızlı cevap verebiliyor olması o yazılımcı için ciddi bir önem taşımaktadır. Hatta kimi zamanlarda, yazılımcının önceden yaptığı bazı şeylerin yöntemlerini değiştirip bunların testlerini baştan yapması gerekir. İşte bu gibi durumlarda, yazılımcının herşeyi eliyle test ediyor olması hem bir zaman kaybıdır hem de hataya açık bir yöntemdir. Tabi bunun yanında hem eski kodların çalışabilirliğini kontrol etmek için hem de ihtiyaç kadar geliştirme yapabilmek için bazı yöntemler sunulmuştur. Bu yazıda onlara değineceğim.
Test Driven Development Nedir?
Test Driven Development (TDD), isminden de anlaşılacağı gibi test bazlı bir yazılım geliştirme yöntemidir. Bu yöntem, basit ve kısa adımlarla, önceden tasarlanmış senaryoları hızlı bir şekilde koda dökmeyi hedeflemektedir. Bunun yanında bu yöntemin pek çok avantajı da geliştirme sırasında fark edilebilir, ancak bunlara yazının ilerleyen kısımlarında değinmek istiyorum.
TDD, yazılımcıları basit ve küçük parçalardan oluşan tasarımlar yapmaya zorlamaktadır. Çünkü TDD'nin temel prensibi kodlama sürecini küçük parçalara bölmek üzerine kuruludur. Kent Beck bu yöntemi anlattığı "Test-Driven Development: By Example" isimli kitabında iki basit konsept üzerinde durmaktadır,
- Elinizde başarısız bir test senaryosu olmadan asla tek satır kod bile yazmayın,
- Kod tekrarı yapmayın.
Bu iki basit madde ileride William Wake'in "Extreme Proramming Explored" adlı kitabında aşağıdaki algoritmaya dönüşmüştür,
- Test kodunu yaz,
- Test kodunu derle, (bu aşamada kodun derlenemiyor olması gerekmektedir. Çünkü test kodunda yazdığımız hiç bir şeyi aslında henüz tanımlamadık)
- Test kodunun derlenmesine yetecek kadar kod yazıp kodu derle,
- Testleri çalıştır ve testlerin başarısız olduğunu gör,
- Sadece testlerin başarılı olmasını sağlayacak kadar tanım yap,
- Testleri tekrar çalıştır ve başarılı olduğundan emin ol,
- Kodun açıklayıcı olması ve tekrarlanmaması için gerekli düzenlemeleri yap,
- Bir sonraki adım için başa dön.
Buradaki adımlardan da görüldüğü üzere tüm tasarım, küçük küçük parçalara bölündüğü gibi her bir özellik de aslında bir o kadar küçük parçalardan oluşmaktadır. Buradaki asıl amaç, yazılımcının sistemli ve düzenli bir şekilde sadece yapmayı hedeflediği küçük parçaya hakim olması ve gerekli testleri bir kere geçtikten sonra da buraya bir daha dönmemesidir. Fakat bu adımlarda bahsedilen bir kaç küçük ayrıntıya da değinmek istiyorum, örneğin "kodun derlenmesine yetecek kadar kod yazmak" tam olarak ne anlama gelmektedir? Ya da testin başarılı olduğunu nasıl anlarım? Aslında bu yazıda buna da değinmek istiyordum ancak Burak Selim Şenyurt'un bu konuda hazırladığı görüntülü anlatımı izlediğimde burada ne dersem cılız kalacağından daha çok vereceğim örnek üzerine gitmemin daha iyi olacağına karar verdim. Bu yazıda bu teknik ile, analizi tamamlanmış bir liste nesnesini yapmaya çalışacağım.
Örnek Proje; Generic List
İlk etapta yapmamız gereken şey, hedefimizi belirlemektedir. Bu yazıda, hepimizin pek çok defa kullandığı .NET kütüphanesindeki generic listenin bir benzerini tasarlamaya çalışacağım. Generic listemiz aşağıdaki özelliklere sahip olmalıdır,
- Liste, tanımlanırken verilen bir tipin koleksiyonu olmalıdır,
- Liste, yaratıldıktan sonra içi boş olmalıdır,
- Listeye yeni bir eleman ekledikten sonra listenin eleman sayısı da bir artmalıdır,
- Listeden bir elemanı sildikten sonra listenin eleman sayısı da bir azalmalıdır,
- Listeden eleman silme işlemi, indeks numarasını vererek ya da elemanın kendisini vererek mümkün olmalıdır.
- Listenin içerisinden bir elemanın indeksini almak istediğimizde sıfır bazlı bir indeks olarak değer dönmeli, eğer bu eleman listenin bir elemanı değilse o zaman -1 dönmelidir.
Buraya kadar basit bir listeyi tanımladık ve aslında işin önemli bir kısmını bitirmiş olduk. Artık yapmamız gereken şey elimizdeki senaryoyu test koduna dökmek. Buna başlamak için Visual Studio ile yeni bir solution yaratıp, ilgili solutiona yeni bir test projesi eklemekle işe başlamamız gerekiyor. Bununla ilgili olarak yukarıda bahsettiğim görüntülü dersi inceleyebilirsiniz. Bundan sonraki ilk işimiz birinci adım için test kodunu yazmak olacaktır,
[TestMethod]
public void InitializationTest()
{
GenericList<string> list = new GenericList<string>();
Assert.AreEqual(list.Count, 0, "Liste ilk yaratıldığında listenin eleman sayısı sıfır olmalıdır.");
}
Aynı şekilde sanki yukarıdaki listedeki maddeleri yazar gibi olmasını istediğimiz kod parçasını yazdık. Şu aşamada elimizde ne GenericList sınıfı ne de o sınıfa ait Count diye bir özellik mevcut. Şu anda sadece GenericList adında bir sınıf olması gerektiğini, bu sınıfın parametresiz yapıcı bir method ile örneklenmesi gerektiğini, bu nesnenin Count diye bir özelliğinin olmasını gerektiğini ve bu nesne yaratılırken bu özelliğin de değerinin sıfır olması gerektiğini yazdık. Bunu yaptıktan sonra projemizi derlemek istediğimizde C# derleyicisi bize derleme anında GenericList<string> gibi bir sınıfın olmadığından bahsedecektir ve tabi ki derlenmeyecektir. Bu problemi çözmek için hemen ilgili sınıfı yaratmaya koyulalım. Bu sınıfı yaratırken de Count diye bir özellik ekleyelim.
public class GenericList<T>
{
private int m_Count = 0;
public int Count
{
get{return m_Count;}
}
}
Evet artık kodumuzu derleyip testi çalıştırdığımızda testi geçtiğimizi görebiliriz. Programımız ilk test senaryomuzu doğruladığına göre bir sonraki adım için başa dönüp, sonraki test senaryosunu yazmamız gerekiyor,
[TestMethod]
public void AddItemTest()
{
GenericList<string> list = new GenericList<string>();
list.AddItem("Yeni bir liste elemanı");
Assert.AreEqual<int>(list.Count, 1, "Listenin eleman sayısı yeni bir eleman eklendiğinde bir artmalıdır.");
}
Yukarıda yine AddItem(string) imzasına sahip bir method varmış gibi düşünüp bu methodu kullandık. Burada da dikkat ederseniz yaptığımız şey, listeye bir eleman ekleyip listenin Count özelliğinin artıp artmadığını kontrol etmek oldu. Yine kodumuzu derlediğimizde hata alacağız. Hatayı gidermek için AddItem(string) imzalı bir methodu da aşağıdaki gibi ekleyebiliriz.
public class GenericList<T>
{
private int m_Count = 0;
public int Count
{
get{return m_Count;}
}
public void AddItem(T item)
{
}
}
Yukarıda görüldüğü gibi kodu derlenebilir hale getirdik ve içeriğini henüz doldurmadık. Burada yapmamız gereken şey testi yeniden çalıştırıp, testi geçemediğini görüp, testi geçmesini sağlayacak kodu yazmak olacaktır. Burada birazdan yapacağım şey aslında basit bir şekilde nesnenin içerisinde bir dizi tutmak ve dizinin elemanlarını eklenen her bir yeni eleman ile birlikte doldurmak olacaktır. Bunun çok fazla detayına girmeden aşağıdaki gibi bir ekleme yapıyorum,
public class GenericList<T>
{
private T[] m_InnerItems = new T[0];
private int m_Count = 0;
public int Count
{
get{return m_Count;}
}
public void AddItem(T item)
{
T[] newItemsArray = new T[m_Count + 1];
Array.Copy(m_innerItems, newItemsArray, m_innerItems.Length);
newItemsArray[m_Count] = item;
this.m_InnerItems = newInnerList;
m_Count++;
}
}
Yukarıda sınıfın içerisinde T tipinden oluşan bir dizi yarattım ve eklenen her bir elemanla birlikte bu diziyi genişletmek için yeni bir dizi yaratıp eski diziyi bunun üzerine kopyaladım. Böylece yeni eklenen elemanı da dizinin en sonuna ekleyip işlemi tamamladım. Bu işlemi de gerçekleştirdikten sonra testimi çalıştırdığımda testin başarıyla tamamlandığını gördüm. Bundan sonra yapmam gereken şey, özellikler listemizdeki bir sonraki özelliğe gidip onu da bu aşamalardan geçirmek olacaktır. Ancak ben bunun tamamını burada anlatarak yazıyı sıkıcı bir hale getirmek istemiyorum. Ancak bu örneğin tamamını buraya tıklayarak indirebilirsiniz.
Eğer dikkat ederseniz yukarıda yazdığımız tüm methodlar ya da parçaları sadece ihtiyacımızı karşılamaya yönelik gelişti. Yani ihtiyacımız olmayan hiç bir şey ile vakit harcamadık, böylece yapmamız gereken işe tam bir konsantrasyon sağlayabildik. Ayrıca ileride Count özelliğini etkileyen bir değişiklik yaptığımızda tüm testleri çalıştırarak önceki yerlerde bir sorun yaşayıp yaşamadığımızı da o anda öğrenebiliyor olacağız. Bunlarda kullandığımız Assert sınfı üzerinde tanımlı olan neredeyse tüm methodlara aynı zamanda bir mesaj da ekliyebiliyor olmamız bize ileride hangi testin ne amaçla yazıldığı konusunda da fikir vermektedir.
Unit Testing bize, yukarıda da bahsettiğim gibi, daha stabil bir çalışma ortamı sağlamaktadır. Bu yüzden mümkün oldukça projelerimizde bunu kullanıyor olmamız projelerimizde hata çıkmasını önlemektedir. Ancak elbette her uygulama yukarıdaki örnek kadar basit olmayabilir. Örneğin kullanıcı arayüzü olan bir projede unit testin nasıl sağlanabilir? Bunun için düşünülmüş ve çözüm olarak sunulmuş tasarım kalıplarından biri olan MVP yi önceki yazımda anlatmıştım. Yani yapmak istediklerimizi ne kadar çok parçalarsak, buna bağlı olarak o kadar test yapmaya uygun bir hale gelmektedir.
Bir sonraki yazımda bu konuya biraz daha detaylı yaklaşacağım. Aynı zamanda biraz daha ileri test methodlarını inceleyip, bu metholarla birlikte bir MVP tasarım kalıbını TDD ile nasıl oluşturabiliriz ondan bahsediyor olacağım. Bir sonraki yazıda görüşmek üzere.