C# Test Driven Development(TDD) Nasıl Kullanılır

29.12.2018

Bir projenin mimarisini oluştururken dikkat etmemiz gereken en önemli hususlardan bir tanesi Test Driven Development(TDD)'dır. Birçok geliştircisi, mimar proje komple sonuçlandıktan sonra test senaryolarını yazmaya başlar. Ancak bir çok mimar tarafından yanlış olarak kabul edilmiştir.

Geliştirme yaparken ilk önce test methodlarımızı yazıp, daha sonra testlerini yazdığımız methodlarımızla geliştirmemize devam etmeliyiz. Biz yazımız tabi küçük örnekler üzerinden ilerleyeceğiz. Konunun mantığını anlamak adına yeterli olacaktır.

Örneğin bir StringHelper yazdık bu StringHelper verilen kelimenin başında, sonunda, ortasında olan boşlukları temizleyecek diyelim. Bunun için test projemizde "StringHelperTests" adında bir class açıyoruz. Bunun içerisinde beklediğimiz üç aksiyon için üç farklı method oluşturuyoruz.

Bir birim testi 3 aşamadan oluşur ;
- Arrange Hazırlıklarımızı yaptığımız kısım. Yani ihtiyacımız olan değişkenleri tanımladığımız kısım.
- Act Hazırlıklarımızı etkileşime geçirdiğimiz yani test etmek istediğimiz methodları cağırdığımız kısım.
- Assert Cağırdığımız methodların sonuçlarını test gönderdiğimiz kısım.

Test classlarımız için üç farklı seviye vardır, bunlar ise şu şekildedir ;

Assembly Level :

1 ) UnitTest üzerinde birden fazla oluşturamayız.
2 ) Bütün UnitTest projelerimiz boyunca tek bir kez çalışacak methodumuz varsa bunu static olarak [AssemblyInitialize] olarak methodu tanımlarız.
3 ) [AssemblyInitialize] başlangıçta UnitTest boyunca bir kez giriyor, UnitTest sonucunda bir kezde [AssemblyCleanup] girecektir.

Test Level :

1 ) Bu methodlara girmeden önce her seferinde yapılmasını istediğimiz bir işlem, method varsa eğer bunu [TestInitialize] olarak tanımlıyoruz.
Örn : Sepet ekleme aksiyonu testimiz ürün ekleme [TestMethod] öncesinde her seferinde sepet oluşturmak yerine [TestInitialize] bölümünde yapabiliriz.

2 ) TestMethod'lardan sonra yapılmasını istediğimiz bir method, işlem varsa eğer bunu da [TestCleanup] olarak tanımlayarak kullanabiliyoruz.
Örn : Sepet ekleme TestMethod kontrollerimizi yaptıktan sonra sepeti temizlemek isteyebiliriz. Bu işlemi burada yapabiliriz.

Class Level :

1 ) Bu methodlara girmeden önce TEK BİR SEFER yapılmasını istediğimiz bir işlem, method varsa eğer bunu [ClassInitialize] static bir method olarak tanımlıyoruz.
Örn : Sepet ekleme aksiyonu testimiz ürün ekleme [TestMethod] static olarak tanımladığımız sepeti her seferinde oluşturmak yerine [ClassInitialize] bölümünde yapabiliriz.

2 ) TestMethod'lardan sonra yapılmasını istediğimiz bir method, işlem varsa eğer bunu da [ClassCleanup] static bir method olarak tanımlayarak kullanabiliyoruz.
Örn : Sepet ekleme TestMethod kontrollerimizi yaptıktan sonra sepeti temizlemek isteyebiliriz. Bu işlemi burada yapabiliriz.

Kulanabileceğimiz "Assert" Methodları ;

AreaEqual : İki değer arasındaki eşitliğin kontrol eder.

 [TestClass]
    public class TrimTests
    {
        [TestMethod]
        public void Trim()
        {
            //Arrange  
            string value = "  S.Ç.  ";//girdiğim değerin başında ve sonunda boşluk var. 
            string expected = "S.Ç."; //bizim almak istediğimiz değer ise başında sonunda boşluk olmadığı hali.

            //Act
            string actual = value.Trim();//Örnek olması açısından Trim() tabi bu bizim yazdığım bir method olmalı.

            //Assert
            Assert.AreEqual(expected, actual, "Trim methodu hatalı");
        }
    }

String eşittir testlerimizde küçük, büyük harf dikkate almak istemiyorsak Assert equal methodunun "ignoreCase" değerini "true" olarak geçebiliriz. Bir bölme işlemi için test yazmak istediğimizde bölüm sonrasında çıkan küsüratı kabul etme gibi bir durum için AreEqual methodumuzda "delta" parametresini kullanabiliriz.

 [TestClass]
    public class DoubleTests
    {
        [TestMethod]
        public void DoubleEqual()
        {
            //Arrange  
            double expected = 1.1111;
            double actual = 1.1112;
 	    double delta = 0.0001;
            Assert.AreEqual(expected, actual, delta, "Divide methodu hatalı");
        }

    }

Burada expected-actual<=delta kontrol edilir. Bu koşula uyması halinde test geçerli olacaktır. 

AreSome : İki obje arasında referans kontrolü yapabiliriz.

 [TestClass]
    public class SameTests
    {
        [TestMethod]
        public void IsSame()
        {
            //Arrange  
            List expected = new List { "Samet", "Çınar" };
            List actual = new List { "Samet", "Çınar" };
           
            Assert.AreSame(expected, actual);
        }

    }

Böyle yapıldığında iki değer birbirine eşit aslında testten geçmeli gibi anlıyoruz. Ancak ikiside farklı bir referans olduğu için AreSame koşulunu karşılamıyor. "AreNotSame" methodunu kullanırsak eğer bu durumda testten geçebiliriz.

[TestClass]
    public class SameTests
    {
        [TestMethod]
        public void IsSame()
        {
            //Arrange  
            List expected = new List { "Samet", "Çınar" };

            List actual = expected;
            actual.Add("Software");
            expected.Add("TDD");


            Assert.AreSame(expected, actual);
        }

    }

But testi böyle uyguladığımızda testin başarılı olduğunu göreceksiniz. İki tane değişken varsa ikisinede farklı eklemeler yapıyoruz ancak ikisi aynı referans(Ram üzerinde tek bir nesne) olduğu için eklemeleri her ikisi içinde yapmış oluyoruz. Böylece "AreSame" testinin gerekliliğini sağlamış oluyor.

IsNull, IsNotNull : Bir değişkenin null olup olmama durumlarını kontrol edebiliriz.

  [TestClass]
    public class NullTests
    {
        [TestMethod]
        public void IsNull()
        {
       
            List value = null;
            Assert.IsNull(value, "Değer null olmalı.");
        }
        [TestMethod]
        public void IsNotNull()
        {

            List value = null;
 
            Assert.IsNotNull(value, "Değer null olamaz");
        }
    }

Null bir değer tanımladık bu değer haliyle "IsNotNull" method testinden geçmeyecek.

 

Kulanabileceğimiz "CollectionAssert" Methodları ;

Assert dışında Collection nesneler için collection elemanlarına bağlı olarak test yapmamızı sağlayan sınıftır.

AreEqual : İki collection arasında itemlar tamamen aynı olmalı ve aynı sırada olmalı. AreEquivalent : İki collection arasında itemlar aynı olması yeterli. Hangi sırada olduğunun her hangi bir önemi yok.

[TestClass]
    public class GenericListTests
    {

        List _expected;
        [TestInitialize]
        public void Initialize()
        {
            _expected = new List { "Samet", "Çınar" };
        }
        [TestMethod]
        public void AreEquivalent()
        {
            List actual = new List { "Çınar", "Samet" };
            CollectionAssert.AreEquivalent(_expected, actual);
        }
        [TestMethod]
        public void AreEqual()
        {
            List actual = new List { "Çınar", "Samet" };
            CollectionAssert.AreEqual(_expected, actual);
        }
    }

Bu test örneğinde her iki method için kullanacağım _excepted nesnesini "[TestInitialize]" attribute verdiğimiz Initialize methodunda tanımladık. Böylece bu test classının tüm methodlarında erişim sağlayabiliyorum.

AreEquivalent methodunda testin başarılı olduğunu ancak AreEqual testinin başarısız olduğunu göreceksiniz. İki method içinde verdiğimiz actual nesnesi aynı ancak birisi sıralamayıda dikkate alıyor(AreEqual), diğeri ise sıralamayı dikkate almıyor.(AreEquivalent)

AllItemsAreNotNull : Liste içerisinde null nesne kontrolü yapar. Yani her hangi bir item null değer barındırıysa test hata vericektir. 

AllItemsAreUnique : Liste içerisinde bulunan değerlerin benzersiz olma durumunu kontrol edecektir.

AllItemsAreInstancesOfType : Liste içerisinde bulunan objelerin tiplerinin aynı olma durumunu kontrol edecektir.

Contains : Bir elementin liste içerisinde var olma durumunu kontrol eder.

IsSubsetOf : X listesi içerisinde Y listesini barındırıyor mu kontrolünü yapar.
 

 [TestClass]
    public class GenericListTests
    {

        List _expected;
        [TestInitialize]
        public void Initialize()
        {
            _expected = new List { "Samet", "Çınar" };
        }
        [TestMethod]
        public void AllItemsAreNotNull()
        {
            CollectionAssert.AllItemsAreNotNull(_expected);
        }
        [TestMethod]
        public void AllItemsAreUnique()
        {
            CollectionAssert.AllItemsAreUnique(_expected);
        }
        [TestMethod]
        public void AllItemsAreInstancesOfType()
        {
            CollectionAssert.AllItemsAreInstancesOfType(_expected, typeof(string));
        }
	[TestMethod]
        public void Contains()
        {
            var element = "Samet"; 
            CollectionAssert.Contains(_expected, element);
        }
        [TestMethod]
        public void IsSubsetOf()
        {
            List element = new List { "Çınar" };
            CollectionAssert.IsSubsetOf(element, _expected);
        }
    }

_expected listemizde her hangi bir null değer olmadığı için "AllItemsAreNotNull" testinden geçti.

_expected listemizde "Samet" ve "Çınar" değerleri birbirinden farklı olduğu için "AllItemsAreUnique" testinden geçti. "Samet","Çınar","Samet" değerleri olsaydı eğer test failed olacaktı.

_expected listemiz zaten string bir List olduğu için string dışında farklı bir item ekleyemezdik. Bu yüzden "AllItemsAreInstancesOfType" testinden geçtik, bir object listemiz olduğunda birden farklı tip alma ihtimali olduğunda bu test daha gerçekci olabilir.

_expected listemizin içerisinde "Samet" adında bir element olduğu için "Contains" testinden geçtik.

_expected listemiz içerisinde "Çınar" adında bir ListItem objesi olduğu için "IsSubsetOf" testinden geçtik. Contains ile arasında farkı Contains'de tek bir değer veriyoruz, IsSubSetOf ile list verip testimizi yapabiliyoruz.

Kulanabileceğimiz "StringAssert" Methodları ;

Yukarda yaptığımız methodlara çok benzer bir yapı olduğu için tek tek anlatma gereği duymuyorum. Bulundurduğu methodları zaten bir kaç tane bunlar ise "Contains,Matches,DoesNotMatch,StartWith,EndsWith" şeklindedir.

StringAssert.Contains ile bir string değer içerisinde istediğimizi değeri bulunduruyor mu testini yapabiliriz, Matches ile ise regex kullanarak bu işlemi yapabiliriz DoesNotMatch'de bunun tam zıttı şeklinde kullanılır. StartWith, EndWith ile de değerimiz X değeri ile başlıyor mu ? bitiyor mu kontrollerini yapabiliriz..

TestContext nedir?

Test uygulamamız belirlediğimiz yerler testin durumunu sorgulamak, hangi aşamada olduğunu öğrenmek için output çıkartmak isteyebiliriz.
Bunun için "TestContext" nesnesini kullanabiliriz. Bu nesne runtime anında kendi oluşacağı için bir classımızda tanımladığımız "TestContext" adı ile kullanmaya direkt başlayabiliriz.

 [TestClass]
    public class GenericListTests
    {
        public TestContext TestContext { get; set; }
        List _expected;
        [TestInitialize]
        public void Initialize()
        {
            TestContext.WriteLine(TestContext.FullyQualifiedTestClassName);
            _expected = new List { "Samet", "Çınar" };
        }
        [TestMethod]
        public void AllItemsAreNotNull()
        {
            TestContext.WriteLine(TestContext.TestName);
            CollectionAssert.AllItemsAreNotNull(_expected);
        }
        [TestCleanup]
        public void Cleanup()
        {
            TestContext.WriteLine(TestContext.CurrentTestOutcome.ToString());
        }
    }

TestContext nesnesi ile test methodu cağırıldığı zaman hangi class üzerinde cağırılmış, methodun adı nedir ve Cleanup ile her method cağırıldıktan sonra girdiği için testin hangi durumunda olduğunu öğrenebiliriz.
Bu işlemlerin çıktısı şu şekilde olacaktır ;

TestContext Messages:
Unit_Test_Test.GenericListTests > Projemizin adı
AllItemsAreNotNull > Kullanılan methodun adı
Passed > Cleanup bölümünde statü bilgisi aldım "Passed" yani başarılı olduğunu gördüm.
 

Samet ÇINAR Hakkında

2010 senesinden bu yana hem tam zamanlı hemde freelance olarak yazılım projelerinde görev almaktayım.
Her gün daha güzel geliştirmeler yapmak için araştırıp öğrenmeyi, öğrendiklerimi aktarmayı çok seviyorum.

İLGİLİ YAZILAR

YORUMLAR

Semih

11.1.2019

bunları böyle toplayıp test senaryosu haline getiriyolar hocam onun örneği var mı acaba

SAMET ÇINAR

14.1.2019

Semih, bu yazdığımız birim testlerini toplu bir senarya ile kullandığımız test için "Integration Test" oluşturmamız gerekiyor. Bu konu başlığı ile araştırma yapabilirsin.

Yorum Yap