Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

.NET Core – Unit Test Nedir? Nasıl Yapılır? Derinlemesine İnceleyelim

Merhaba,

Malum, bir uygulamada, uygulamanın genel kod standardının sınırlarını çizebilmek ve gelişim sürecinde istenilen doğrultuda bir seyr halinde olup olmadığını değerlendirebilmek için süreci kontrol edecek bir akla ve o aklın mukayese için baz alacağı kurallara ihtiyacı olmaktadır. İşte bizler bu akla ve kullanacağı kurallara Unit Test diyoruz. Unit Test, esasında yazılım süreçlerinde dört farklı test yaklaşımından sadece birisidir. Tabi ki de diğerlerine yazımızın devamında kısmi temas edeceğiz lakin şimdilik esas konumuz olan Unit Test’ler üzerinden bir girizgah yapalım…

İçindekiler :

Unit Test Nedir? Ne Amaçla Kullanılır?

Esasında bu tarz soruları sorup cevaplandırdığım Node.js – Unit(Birim) Test Nedir? Nasıl Yazılır? başlıklı makalemi inceleyebilirsiniz. İlgili makale her ne kadar Node.js odaklı yazılmış olsa da Unit Test açısından evrensel nitelikte notlar barındırmaktadır.

Unit Test, Birim Test’i demektir. Yani uygulamadaki en küçük işlem yapan birimlerin/metotların test edilmesidir. Burada amaç, bir uygulamadaki metotların önceden belirlenmiş kurallar çerçevesinde davranışlarının test edilmesidir. Böylece, bireysel yahut ekipsel çalışmalar da inşa edilen kodların belirlenmiş standartlar çerçevesinde olması sağlanmış olacak ve yapılacak değerlendirmelerden geçemeyen birimler standartlara uymadıklarından dolayı hızlıca kontrol edilip onarılabilecektirler.

Genellikle uygulamada yazılacak metotların hangi standartlarda geliştirilmesi gerektiğinin kurallarını çizen Unit Test yaklaşımı işin gelişim ilkelerini belirlerken, biryandan da yapısal olarak süreçte olabilecek algoritmik değişikliklerin neticesinde kuralların aşılıp aşılmadığını da hızlı bir şekilde test edebilmemizi amaçlamakta ve böylece etkili ve güzel bir çözüm getirmemizi sağlamaktadır.

Unit test’ler bir uygulama için önceden belirlenen standartlar eşliğinde erken uyarı sistemidir.

Test Çeşitleri Nelerdir?

Unit Test, yazılım süreçlerindeki dört test çeşidinden sadece bir tanesidir.
.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim
Görüldüğü üzere yazılım süreçlerindeki testler yukarıdaki gibidir. Bu görselde testler önemlilik seviyesi aşağıdan yukarıya doğru olacak şekilde hizalanmıştır ve bizlerde bu hizada ufaktan açıklamalarını yapalım.

  • Unit Tests
    Metotların testidir. Teknik olarak en yoğun test türüdür.
  • Integration Tests
    Uygulamadaki modüllerin birbirleriyle nasıl davranış sergilediklerinin kontrol edildiği test türüdür.
  • End to End Tests
    Bir uygulamanın baştan sona kadar nasıl davranış sergileyeceğinin test edilmesidir.
  • UI Tests
    Kullanıcı arayüzünün test edilmesidir.

.NET Core’da Unit Test Nasıl Yapılır?

.NET Core’da Unit Test yapabilmek için öncelikle test edilecek bir uygulamaya ihtiyacınız vardır. Bu uygulama herhangi bir Console App, Web App veya API olabilir. Ardından ilgili uygulamayı test edebileceğimiz ve kuralları koyup değerlendirebileceğimiz bir test projesine ihtiyaç vardır. İşte burada devreye test için kullanılacak framework’ler girmektedir. Piyasada bu konuda kullanılabilecek bir çok test framework’ü mevcuttur(bknz : MSTest, NUnit veya xUnit.net)

Bizler genellikle testlerimizi open source olan xUnit.Net Framework’ü ile geliştirmeyi tercih ediyoruz. Haliyle bu içeriğimizde de xUnit.Net üzerinden devam edeceğiz.

xUnit.Net Projesi Nasıl Oluşturulur ve Test Temelleri Nelerdir?

Öncelikle test edilecek farazi bir uygulamamız ve bu uygulama içerisinde aşağıdaki gibi bir matematik işlemlerini gerçekleştiren sınıfımız olsun.

    public class Mathematics
    {
        public int Sum(int number1, int number2)
         => number1 + number2;
        public int Subtract(int number1, int number2)
         => number1 % number2;
        public int Multiply(int number1, int number2)
         => number1 * number2;
        public int Divide(int number1, int number2)
         => number1 / number2;
    }

Şimdi bu sınıf içerisindeki metotlardan isimlerine uygun bir şekilde matematiksel işlemleri yapması beklenmektedir. Bunun için gerekli bir kontrol ve değerlendirme sağlanmalıdır. Tabi ki de buradaki değerlendirmeyi gözümüzle yapmaya kalktığımız zaman olabilecek muhtemel hataları yüksek ihtimal göremeyebiliriz. Halbuki koda detaylı bakarsanız ‘Subtract’ isimli metotda çıkarma işlemi yerine mod alma işlemi yapılmıştır. İşte gözle bu şekilde görülemeyecek derecede basit ama sonuç açısından etkili hataları test süreçlerinde hızlıca fark edebilmekte ve production’a çıkmadan manipüle edebilmekteyiz.

Şimdi gelin bu sınıfı test edecek olan aynı solution’da bir xUnit.Net projesi oluşturalım. Bunun için Visual Studio’da direkt olarak aşağıdaki görselde olduğu gibi xUnit Test Project template’ini seçmemiz yeterli olacaktır.
.NET Core - Unit Test Nedir? Nasıl Yapılır? Derinlemesine İnceleyelim
Eklediğimiz xUnit Test projesinin içerisinde kullanılan kütüphanelere kısaca göz atmakta fayda var.
.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelimMicrosoft.NET.Test.Sdk : xUnit.Net ile yazılmış kodların build edilmesini sağlayan SDK’dır.
xunit : xUnit.Net içerisinde kullanılacak tüm member’lar bu pakettedir.
xunit.runner.visualstudio : xUnit.Net ile yazılan test kodlarının Test Explorer penceresinde çalıştırılabilmesini sağlayan pakettir.

Ve en önemlisi testte tabi tutulacak olan proje, test projesi tarafından referans edilmelidir ki kodlarına erişilebilsin ve test gerçekleştirilebilsin. Haliyle ‘Projects’ kısmına bakarsanız test projesi olan ‘TestExample.Test’ tarafından test edilecek olan ‘TestExample’ projesi referans edilmiştir.

Evet… Şimdi teste geçebilir ve test temellerini inceleyebiliriz.

Unit test yazmak Arrange, Act ve Assert olmak üzere üç aşamadan oluşur!

  • Arrange
    Test edilecek metodun kullanacağı kaynakların hazırlandığı bölümdür. Değişken tanımlama, nesne oluşturma vs. gerçekleştirilir.
  • Act
    Arrange aşamasında hazırlanan değişkenler yahut nesneler eşliğinde test edilecek olan metodun çalıştırıldığı bölümdür. Mümkün mertebe kısa ve öz olması makbuldür.
  • Assert
    Act aşamasında yapılan testin doğrulama evresidir. Tek bir Act’te birden fazla sonuç gerçekleşebilir. Misal olarak; exception fırlatılabilir yahut herhangi bir türde result dönebilir.

Bu terimler eşliğinde ‘Mathematics’ sınıfındaki metotları test edebilmek için ‘Test’ projemizde bir ‘MathematicsTest’ isminde bir class oluşturalım ve aşağıdaki gibi şimdilik ‘Subtract’ metodunu test eden kodları inşa edelim.
‘Mathematics’ sınıfındaki ‘Subtract’ metodu;

    public class Mathematics
    {
        .
        .
        .
        public int Subtract(int number1, int number2)
         => number1 % number2;
        .
        .
        .
    }

‘MathematicsTest’ sınıfındaki ‘Subtract’ metodunu test eden ‘SubtractTest’ metodu;

    public class MathematicsTest
    {
        [Fact]
        public void SubtractTest()
        {
            #region Arrange
            //Kaynaklar hazırlanıyor.
            int number1 = 10;
            int number2 = 20;
            int expected = -10;
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            //İlgili metot Arrange'de ki kaynaklarla test ediliyor.
            int result = mathematics.Subtract(number1, number2);
            #endregion
            #region Assert
            //Test neticesinde gelen data doğrulanıyor.
            Assert.Equal(expected, result);
            #endregion
        }
    }

Test süreçlerinde ilgili metot herhangi bir parametre almıyorsa eğer ‘Fact’ attribute’u ile işaretlenmelidir.

.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelimYapılan bu test işlemini kontrol edebilmek için Visual Studio editöründe harika bir tool mevcuttur. ‘View’ -> ‘Test Explorer’

‘Test Explorer’, ‘Fact’ ile işaretlenmiş metotların test metotları olduğunu algılamakta ve içerisindeki ‘Assert’ değerlendirmelerine göre ilgili testin sonucunu aşağıdaki gibi bizlere sunmaktadır.
.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelimGörüldüğü üzere ilgili tool’da sol üst köşedeki yeşil butonlar aracılığıyla tüm test metotları çalıştırılır ve netice olarak bizlere değerlendirme sunulmuş olur.

Burada dikkat ederseniz, beklenen(expected) ‘-10′ değeri iken gelen(actual) ’10’ değeridir. Dolayısıyla bu metot(SubtractTest) testten geçememiştir. İşte bu durumda bizler bir sıkıntı olduğunun farkına varıyor ve ilgili metotta çıkarma işlemi yerine mod işlemi yapıldığını nokta atışıyla yakalayabiliyoruz.
 
 
 
 

Peki, test durumunda beklenen ve gelen değerin dışında başka durumları kontrol edebiliyor muyuz?
Tabi ki de… Bunun için ‘Assert’ sınıfının fonksiyonlarını incelemekte fayda olacaktır.

Fonksiyon Açıklama
Equal/NotEqual Test sürecinde gelen sonuçla, beklenen sonucu kıyaslamamızı sağlayan metottur.
            Assert.Equal(expected, result);
            Assert.NotEqual(expected, result);
Contain/DoesNotContain Test sürecinde gelen sonuç içerisinde bir değerin olup olmamasını kontrol eden metotlardır.
            var containsValues = new[] { 3, 5, 7, -10 };
            var doesNotContainsValues = new[] { 2, 4, 6 };
            Assert.Contains<int>(containsValues, value => value == result);
            Assert.DoesNotContain<int>(doesNotContainsValues, value => value == result);
True/False Test sürecinde şartın doğrulandığını(True) yahut yanlışlandığını(False) kontrol eden metotlardır.
            Assert.True(result < 10);
            Assert.False(result > 10);
Match/DoesNotMatch Test sürecinde gelen değerin bildirilmiş olan Regex ifadesine uyup uymadığını kontrol eden metotlardır.
            Assert.Matches("sa{2}t", "saat");
            Assert.DoesNotMatch("sa{2}t", "muiddin");
StartsWith/EndsWith Test sürecinde gelen değerin bildirilmiş olan değerle başlayıp bittiğini kontrol eden metotlardır.
            Assert.StartsWith("G", "Gençay");
            Assert.EndsWith("y", "Gençay");
Empty/NotEmpty Test sürecinde gelen koleksiyonel değerlerin boş olup olmama durumunu kontrol eden metotlardır.
            var emptyCollection = new List<object>();
            var notEmptyCollection = new List<object>() { 3 };
            Assert.Empty(emptyCollection);
            Assert.NotEmpty(notEmptyCollection);
InRange/NotInRange Test sürecinde gelen değerin belirli bir aralıkta olup olmamasını kontrol eden metotlardır.
            Assert.InRange<int>(result, -1000, 1000);
            Assert.NotInRange<int>(result, 1000, 2000);
Single Test sürecinde gelen koleksiyonel verinin içerisinde sadece tek bir data olup olmadığını kontrol eden metottur.
            var collection = new List<object> { 3 };
            Assert.Single(collection);
IsType/IsNotType Test sürecinde gelen değerin türüne göre kontrol sağlayan metotlardır.
            Assert.IsType<int>(result);
            Assert.IsNotType<string>(result);
IsAssignableFrom Test sürecinde gelen değerin hangi türden türediğini kontrol eden metottur.
            Assert.IsAssignableFrom<object>(result);
            //ya da
            var collection = new List<object>();
            Assert.IsAssignableFrom<IEnumerable<object>>(collection);
Null/NotNull Test sürecinde gelen değerin null olup olmama durumunu kontrol eden metotlardır.
            Assert.Null(result);
            Assert.NotNull(result);


Test metotları parametre almıyorsa eğer ‘Fact’ ile işaretliyoruz… Peki ya parametre alıyorsa?
Parametre alan metotları ise ‘Theory’ attribute’u ile işaretliyor ve ardından parametrelerini ‘InlineData’ attribute’u ile opsiyonel olarak veriyoruz. Bunun için aşağıdaki örnek kodu inceleyiniz;

        [Theory]
        [InlineData(3, 5, 8)]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }

.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim

İlgili metodu farklı değerlerle test edecekseniz eğer birden fazla ‘InlineData’ attribute’unu aşağıdaki gibi kullanabilirsiniz.

        [Theory]
        [InlineData(3, 5, 8)]
        [InlineData(11, 5, 16)]
        [InlineData(23, 2, 25)]
        [InlineData(33, 44, 87)]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim

Görüldüğü üzere sonuncu InlineData parametrelerinde 33 ile 44’ün toplamı 87 etmeyeceğinden dolayı testten geçememektedir.


Eğer ki test edilecek data miktarı haddinden fazla ise ‘InlineData’ ile bunu yapmak mümkün olsa da yersiz kod maliyetine sebep olabilmektedir. Böyle durumlarda ‘MemberData’ attribute’unu kullanabilirsiniz.

        public static IEnumerable<object[]> sumDatas => new List<object[]> {
            new object[]{ 3, 5, 8 },
            new object[]{ 11, 5, 16 },
            new object[]{ 23, 2, 25 },
            new object[]{ 33, 44, 87 }
        };

        [Theory]
        [MemberData(nameof(sumDatas))]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }

‘MemberData’ attribute’u ile kullanılacak member türü IEnumerable<object[]> ve static olmak zorundadır. İlgili yapıda belirtilen member üzerinden test sürecinde istenilen değerler parametre olarak hızlıca geçilebilmektedir.


Eğer ki bu member farklı bir class’ın üyesi olsaydı aşağıdaki gibi ‘MemberType’ property’si üzerinden sınıf bildiriminde bulunarak çalışma yapılması yeterli olacaktı;

    public class Datas
    {
        public static IEnumerable<object[]> sumDatas => new List<object[]> {
            new object[]{ 3, 5, 8 },
            new object[]{ 11, 5, 16 },
            new object[]{ 23, 2, 25 },
            new object[]{ 33, 44, 87 }
        };
    }
    public class MathematicsTest
    {
        [Theory]
        [MemberData(nameof(Datas.sumDatas), MemberType = typeof(Datas))]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
    }


Ayrıca ‘MemberData’ ile yapılan test süreçlerinde veri seti yüzlerce yahut binlerce olanlar için ‘Test Explorer’da tek bir sonuç elde etmek istiyorsak eğer “DisableDiscoveryEnumeration” özelliğine ‘true’ vermemiz yeterli olacaktır.

        [Theory]
        [MemberData(nameof(Datas.sumDatas), MemberType = typeof(Datas), DisableDiscoveryEnumeration = true)]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }

.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim
Haliyle bu şekilde veri kümesindeki tüm değerlerin doğrulanması durumunda ancak testten geçebileceği aşikardır.


Ayrıca ‘MemberData’ attribute’una alternatif olarak ‘ClassData’ attribute’unu da kullanabiliriz.

    public class Datas : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { 3, 5, 8 };
            yield return new object[] { 11, 5, 16 };
            yield return new object[] { 23, 2, 25 };
            yield return new object[] { 33, 44, 87 };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    public class MathematicsTest
    {
        [Theory]
        [ClassData(typeof(Datas))]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
    }

Tabi ‘ClassData’ attribute’u, dataları alacağı sınıfa IEnumerable<object[]> arayüzünü implemente etmemizi ve ‘GetEnumerator’ içerisinde yield ile dataları itere etmemizi istemektedir.


Peki, IEnumerable<object[]> yerine IEnumerable<string[]> yahut IEnumerable<int[]> gibi tip güvenli test verilerini nasıl yazabiliriz?
Bunun içinde ‘TheoryData’ attribute’undan faydalanabiliriz. Şöyle ki;

    public class TypeSafeData : TheoryData<int, int, int>
    {
        public TypeSafeData()
        {
            Add(3, 5, 8);
            Add(11, 5, 16);
            Add(23, 2, 25);
            Add(33, 44, 87);
        }
    }
    public class MathematicsTest
    {
        [Theory]
        [ClassData(typeof(TypeSafeData))]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Arrange
            Mathematics mathematics = new Mathematics();
            #endregion
            #region Act
            int result = mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
    }

Görüldüğü üzere tip güvenliğini kullanabilmek için yukarıdaki gibi ‘TheoryData’ class’ından faydalanabilmekteyiz.

Daha Profesyonel Bir Yaklaşımla Test İnşası Nasıl Yapılır?

Şu ana kadar tüm ‘Arrange’ler de teste tabi tutulacak ‘Mathematics’ sınıfının instance’larını manuel tek tek oluşturuyorduk. Dolayısıyla bu test kodları açısından oldukça maliyetli bir yaklaşım. Bizler bu instance’ı tek seferde constructor üzerinden de oluşturabiliriz.

    public class MathematicsTest
    {
        Mathematics _mathematics;
        public MathematicsTest()
        {
            _mathematics = new();
        }
        [Theory]
        [ClassData(typeof(TypeSafeData))]
        public void SumTest(int number1, int number2, int expected)
        {
            #region Act
            int result = _mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
        [Fact]
        public void SubtractTest()
        {
            #region Arrange
            int number1 = 10;
            int number2 = 20;
            int expected = -10;
            #endregion
            #region Act
            int result = _mathematics.Subtract(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
        [Theory, InlineData(3, 5)]
        public void MultiplyTest(int number1, int number2)
        {
            #region Act
            int result = _mathematics.Multiply(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(15, result);
            #endregion
        }
        [Theory, InlineData(30, 5, 6)]
        public void DivideTest(int number1, int number2, int expected)
        {
            #region Act
            int result = _mathematics.Divide(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
    }

Burada özellikle dikkat edilmesi gereken nokta şudur ki, xUnit.Net her birim testi için bulunduğu sınıfın bir instance’ını almaktadır. Haliyle bu örnekte dört farklı metot olduğuna göre test sürecinde dört farklı nesne oluşturuldu demektir.

Dolayısıyla xUnit.Net, her bir birim için instance üretmesinden dolayı bir test bitmeden diğerine geçmeyecektir.


Peki buradaki instance üretim maliyetini düşürebilir miyiz?
xUnit.Net’te üretilecek olan instance’ların üretim maliyetini düşürebilmek için ilgili sınıftan tek bir nesne üretilmeli ve tüm birim testlerinde o nesne kullanılmalıdır. Bunun için ‘IClassFixture’ interface’i kullanılabilir.

    public class MathematicsTest : IClassFixture<Mathematics>
    {
        Mathematics _mathematics;
        public MathematicsTest(Mathematics mathematics)
        {
            _mathematics = mathematics;
        }
        [Theory]
        [ClassData(typeof(TypeSafeData))]
        public void SumTest(int number1, int number2, int expected)
        {
            Task.Delay(5000).Wait();
            #region Act
            int result = _mathematics.Sum(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
        [Fact]
        public void SubtractTest()
        {
            Task.Delay(5000).Wait();
            #region Arrange
            int number1 = 10;
            int number2 = 20;
            int expected = -10;
            #endregion
            #region Act
            int result = _mathematics.Subtract(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
        [Theory, InlineData(3, 5)]
        public void MultiplyTest(int number1, int number2)
        {
            Task.Delay(5000).Wait();
            #region Act
            int result = _mathematics.Multiply(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(15, result);
            #endregion
        }
        [Theory, InlineData(30, 5, 6)]
        public void DivideTest(int number1, int number2, int expected)
        {
            Task.Delay(5000).Wait();
            #region Act
            int result = _mathematics.Divide(number1, number2);
            #endregion
            #region Assert
            Assert.Equal(expected, result);
            #endregion
        }
    }

Görüldüğü üzere test sınıfını bu şekilde ‘IClassFixture’ arayüzünden implemente ettirmemiz instance maliyeti açısından yeterli olacaktır. Geri kalanını xUnit.Net halledecektir.


Peki, testler paralel bir şekilde çalıştırılabilir mi?
Testleri paralel çalıştırabilmek için her bir unit testi ayrı bir sınıfa almamız gerekecektir.

    public class Test
    {
        [Fact]
        public void Test1()
        {
            Thread.Sleep(5000);
        }
        [Fact]
        public void Test2()
        {
            Thread.Sleep(5000);
        }
    }

Yukarıdaki yapılanmayı ele alırsak her bir test için 5’er saniye bekleyeceği için totalde 10 saniyelik bir maliyet oluşacaktır.

    public class TestA
    {
        [Fact]
        public void Test1()
        {
            Thread.Sleep(5000);
        }
    }
    public class TestB
    {
        [Fact]
        public void Test2()
        {
            Thread.Sleep(5000);
        }
    }

Her iki testi’de ayrı class’lara alırsak iki sınıf içinde instance oluşturulacak ve test süreci paralel başlatılacaktır. Dolayısıyla totalde her iki test’te 5’er saniye paralelde yürütüleceği için 5 saniyelik bir maliyet oluşacaktır.


xUnit.Net kütüphanesi burada her bir class’ı farklı bir ‘Collection’ olarak değerlendirmektedir. ‘Collection’ı esasında bir instance ile ayağa kaldırılacak test topluluğu gibi düşünebiliriz.

Hatta ayrı sınıfları tek bir collection altında toplayarak yine tek bir instance üzerinden single thread’de çalışmasını sağlayabiliriz.

    [Collection("Collection1")]
    public class TestA
    {
        [Fact]
        public void Test1()
        {
            Thread.Sleep(5000);
        }
    }
    [Collection("Collection1")]
    public class TestB
    {
        [Fact]
        public void Test2()
        {
            Thread.Sleep(5000);
        }
    }

Tabi ki de bu durum pek tercih edilir olmasa gerek… Nihayetinde ne kadar çok class’ı tek bir collection altında toplarsak testlerin tamamlanma süresi o kadar uzayacaktır.

Moq Framework Nedir?

Test edilmek istenilen sınıfların gerçek nesnelerini kullanmak yerine onları simüle etmemizi sağlayan ve böylece test süreçlerindeki maliyetleri minimize etmemizi hedefleyen bir framework’tür.

Test edilmek istenilen sınıfların Moq framework’ü ile simüle edilmesine Mocking denmektedir?

Misal olarak, test edilecek bir sınıfın haddinden fazla uzun süreliğine çalışan metotları olduğunu düşünelim. Böyle bir durumda ne yapacağız? Metotların test süreçlerinde de işlevlerinin bitmesini mi bekleyeceğiz? Tabi ki de hayır! Nihayetinde bu şekilde çalışan onlarca metodumuz olabilir ve her birini beklemek demek dakilar, saatler ve hatta günler demek olabilir.

İşte böyle bir durumda ilgili sınıfları simüle etmek ve sadece davranışlarını modellemek testten geçip geçmediğine dair değerlendirme yapmak için yeterli olacaktır.

Haliyle bunun için ilgili test uygulamasına Moq Framework‘ünü yüklemeniz ve yazımızın devamında ele alınacağı gibi metotlarından faydalanmanız yeterli olacaktır.


Peki bir sınıfı nasıl Mocking yapabiliriz?
Bir sınıfı mocklayabilmek için o sınıfın implemente ettiği interface kullanılmalıdır. Aksi taktirde bunun dışında bir yapılanmayı denemek ilgili uygulamanın derleme aşamasında hata almasına sebep olacaktır…

Şimdi aşağıdaki ‘Sum’ metodunu ele alırsak eğer görüldüğü üzere toplama işlemini filanca sebeplerden dolayı 5 saniye zarfında yaptığını görmekteyiz 🙂 Haliyle her test sürecinde bu 5 saniyeyi beklemek mecburiyetinde kalacağız.

    public interface IMathematics
    {
        int Sum(int number1, int number2);
    }
    public class Mathematics : IMathematics
    {
        public int Sum(int number1, int number2)
        {
            Thread.Sleep(5000);
            return number1 + number2;
        }
    }

Halbuki ‘Sum’ metodunu taklit ederek netice itibariyle aşağıdaki gibi 5 saniye beklemeksizin direkt olarak test kontrolünü gerçekleştirebiliriz.

    public class MathematicsTest
    {
        [Fact]
        public void SumTest()
        {
            var mathematics = new Mock<IMathematics>();
            mathematics.Setup(m => m.Sum(1, 2))
                .Returns(3);
            int result = mathematics.Object.Sum(1, 2);

            Assert.Equal(3, result);
        }
    }

.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelimYukarıdaki kod parçasına göz atarsanız eğer 6. satırda ‘Mock’ sınıfına generic olarak ‘IMathematics’ interface’i verilmekte ve böylece hangi interface içerisindeki metotların simüle edileceği bildirilmiş olunmaktadır. 7. satırda ise ilgili interface içerisinde simüle edilecek olan metot ‘Setup’ edilmekte ve böylece simüle sürecinde verilecek ‘1’ ve ‘2’ parametre değerlerine karşı geriye ‘3’ değerinin dönmesi gerektiği bildirilmektedir. 9. satırda ise artık simülasyon ayarları bitmiş olan ‘Mock’ nesnesi üzerinden ‘Object’ property’si ile üretilen nesne çağrılmakta ve ilgili metot tetiklenmektedir. Ve sonuç olarak ilgili metot yandaki gibi taklidi üzerinden milisaniye cinsinden kısa bir sürede test edilmiş olacaktır.


Peki Mock sınıfı üzerinden ayrı değerlendirmeler yapabiliyor muyuz?
Evet… Misal Mock sınıfı üzerinden bir metodun kaç kez çalıştığını yahut metodun exception fırlattığını test edebilirsiniz.

  • Verify
    Bir metodun kaç kez çalıştığını test edebilmek için kullanılan metottur.

        public class MathematicsTest
        {
            [Fact]
            public void SumTest()
            {
                var mathematics = new Mock<IMathematics>();
                mathematics.Setup(m => m.Sum(1, 2))
                    .Returns(3);
                int result = mathematics.Object.Sum(1, 2);
    
                Assert.Equal(3, result);
    
                mathematics.Verify(x => x.Sum(1, 2), Times.AtLeast(2));
            }
        }
    

    Yukarıdaki kod bloğunu incelerseniz eğer 13. satırda ‘Sum’ metodunun iki kere çalıştırılması durumunda testten geçeceği bildirilmiştir. Ayrıca ‘Verify’ metodu sayesinde teste tabi tutulan metodun içerisinde çalıştırılan farklı metotlarında çalıştırılıp çalıştırılmadığını test edebiliriz.

        public interface ISample
        {
            int Check();
            void Handler();
        }
        public class Sample : ISample
        {
            public int Check()
            {
                return 5;
            }
            public void Handler()
            {
    
            }
        }
        public class SampleService
        {
            ISample _sample;
            public SampleService(ISample sample)
            {
                _sample = sample;
            }
            public int Check()
            {
                _sample.Handler();
                return _sample.Check();
            }
            public void Handler() => _sample.Handler();
        }
    
        public class SampleTest
        {
            Mock<ISample> _mockSample;
            SampleService _sampleService;
            public SampleTest()
            {
                _mockSample = new Mock<ISample>();
                _sampleService = new SampleService(_mockSample.Object);
            }
            [Fact]
            public void Sample_Test()
            {
                _mockSample.Setup(s => s.Check()).Returns(5);
                _sampleService.Check();
                _mockSample.Verify(s => s.Handler(), Times.Once());
            }
        }
    

    .NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim

  • Throws
    Bir metodun fırlattığı exception’ı test edebilmemizi sağlayan metottur.

        public interface IMathematics
        {
            int Divide(int number1, int number2);
        }
        public class Mathematics : IMathematics
        {
            public int Divide(int number1, int number2)
             => number1 / number2;
        }
    

    Yukarıdaki bölme işlemini test edersek eğer;

        public class MathematicsTest
        {
            [Fact]
            public void DivideTest()
            {
                Mathematics mathematics = new Mathematics();
                var mathematicsMock = new Mock<IMathematics>();
                mathematicsMock.Setup(m => m.Divide(1, 0))
                    .Throws<DivideByZeroException>();
    
                var exception = Assert.Throws<DivideByZeroException>(() => mathematics.Divide(1, 0));
            }
        }
    

    şeklinde işlem gerçekleştirebiliriz. Burada ‘Divide’ metodu eğer ki ‘1’ ve ‘0’ değerlerine karşılık ‘DivideByZeroException’ fırlatıyorsa test başarılıdır. Haliyle herhangi bir sayı sıfıra(0) bölünemeyeceğinden beklenen hata fırlatılacak ve test başarılı olacaktır.


Peki Mock ile yapılan simülasyonda opsiyonel parame kullanabilir miyiz?
Evet. Bunun için It.IsAny<T> generic türünden faydalanmaktayız.

Bunun için aşağıdaki örnek kod bloğunu inceleyiniz..

    public class MathematicsTest
    {
        [Fact]
        public void SumTest()
        {
            int result = 0;

            var mathematics = new Mock<IMathematics>();
            mathematics.Setup(m => m.Sum(It.IsAny<int>(), It.IsAny<int>()))
                .Callback<int, int>((number1, number2) => result = number1 + number2);
          
            mathematics.Object.Sum(1, 2);
            Assert.Equal(3, result);

            mathematics.Object.Sum(5, 5);
            Assert.Equal(10, result);

            mathematics.Object.Sum(15, 5);
            Assert.Equal(20, result);

            mathematics.Object.Sum(23, 2);
            Assert.Equal(25, result);
        }
    }

9. satırda ‘Setup’ edilen ‘Sum’ fonksiyonunun parametreleri ‘It.IsAny<T>’ ile işaretlenerek optisonel olarak int türünden değerler alacağı bildirilmekte ve ‘Callback’ fonksiyonu ile bu alınan değerler üzerinde yapılacak işlem belirtilmektedir. Burada gelen değerler toplanmakta ve sonuçları ‘result’ değişkenine atanmaktadır. Bu aşamadan sonra artık ‘Object’ üzerinden çağrılan her bir ‘Sum’ fonksiyonu, verilen değerlere göre ‘result’ değişkenine işlem sonucunu assign edecektir. Haliyle her bir Assert bu sonuçlara göre değerlendirilmektedir.

Misal, bazen de belli bir aralıkta gelecek olan değerlere göre sürekli sabit bir işlem gerçekleştirebilir yahut değer dönebiliriz. Bunun içinde It.IsInRange<int> kullanılabilir.

    public class MathematicsTest
    {
        [Fact]
        public void SumTest()
        {
            int result = 0;

            var mathematics = new Mock<IMathematics>();
            //Moq.Range.Inclusive : 1 ile 10'da dahil.
            //Moq.Range.Exclusive : 1 ile 10'da dahil değil.
            mathematics.Setup(m => m.Sum(It.IsInRange<int>(1, 10, Moq.Range.Inclusive), It.IsInRange<int>(1, 10, Moq.Range.Inclusive)))
                .Returns(-5);

            mathematics.Object.Sum(1, 2);
            Assert.Equal(-5, result); //Ok

            mathematics.Object.Sum(5, 5);
            Assert.Equal(-5, result); //Ok

            mathematics.Object.Sum(15, 5);
            Assert.Equal(-5, result); //Fail

            mathematics.Object.Sum(23, 2);
            Assert.Equal(-5, result); //Fail
        }
    }

Ayrıca ‘SetupSequence’ ile bir metodu her çağrıldığında farklı değerler döndürecek şekilde mocklayabiliriz.

    public interface ISample
    {
        int Check();
    }
    public class Sample : ISample
    {
        public int Check()
        {
            return 5;
        }
    }
    public class SampleTest
    {
        [Fact]
        public void Sample_Test()
        {
            var sample = new Mock<ISample>();
            sample.SetupSequence(m => m.Check())
                .Returns(-5)
                .Returns(-10)
                .Returns(15);

            var result1 = sample.Object.Check(); //-5
            var result2 = sample.Object.Check(); //-10
            var result3 = sample.Object.Check(); //15

            Assert.Equal(-5, result1);
            Assert.Equal(-10, result2);
            Assert.Equal(15, result3);
        }
    }


Peki Mock sınıfı ile property’leri simüle edebiliyor muyuz?
Tabi ki de… Hemen örneklendirelim :

    public class SampleTest
    {
        [Fact]
        public void Sample_Test()
        {
            var sampleMock = new Mock<ISample>();
            //İlk değer 125 olarak atanıyor.
            sampleMock.SetupProperty(s => s.SampleProperty, 125);
            //Tabi süreçte farklı değerde set edebiliyoruz.
            sampleMock.Object.SampleProperty = 55;
            var result = sampleMock.Object.SampleProperty;
            Assert.Equal(55, result);
        }
    }

Eğer ki test edilecek olan property sayısı haddinden fazlaysa her birini tek tek bu şekilde setuplamamak için aşağıdaki gibi ‘SetupAllProperties’ini de kullanabiliriz.

            var sampleMock = new Mock<ISample>();
            sampleMock.SetupAllProperties();


Peki setter’ı olmayan property’leri nasıl simüle edebiliyoruz?
Bunun içinde ‘SetupGet’ metodu kullanılabilmektedir.

    public class SampleTest
    {
        [Fact]
        public void Sample_Test()
        {
            var sampleMock = new Mock<ISample>();
            sampleMock.SetupGet(x => x.SampleProperty)
                .Returns(55);
            var result = sampleMock.Object.SampleProperty; //55
            Assert.Equal(55, result);
        }
    }

Burada readonly olan ‘SampleProperty’ property’si get edildiğinde geriye ’55’ değerini dönecek şekilde simüle edilmiştir.

Ayrıca property’e istediğimiz değerin set edilip edilmediğini kontrol etmemiz gerekirse eğer;

    public class SampleTest
    {
        [Fact]
        public void Sample_Test()
        {
            var sampleMock = new Mock<ISample>();
            sampleMock.SetupProperty(x => x.SampleProperty);
            sampleMock.Object.SampleProperty = 55;

            sampleMock.VerifySet(x => x.SampleProperty = 55);
        }
    }

şeklinde çalışmamız yeterli olacaktır.

Bunların dışında herhangi bir property’nin değerinin read edilip edilmediğini de aşağıdaki gibi kontrol edebiliriz;

    public class SampleTest
    {
        [Fact]
        public void Sample_Test()
        {
            var sampleMock = new Mock<ISample>();
            sampleMock.SetupProperty(x => x.SampleProperty);
            sampleMock.Object.SampleProperty = 55;
            //Burada property değerini get ediyoruz.
            var result = sampleMock.Object.SampleProperty;
            //Dolayısıyla aşağıdaki doğrulama başarılı olacak ve testten geçilecektir.
            sampleMock.VerifyGet(x => x.SampleProperty);
        }
    }

Komut Satırından Test Çalıştırma Nasıl Yapılır?

Ve son olarak, bazen testlerimizi komut satırından çalıştırmamız gerekebilir. Bunun için, ilgili test projesinin dizinine powershell yahut cmd üzerinden odaklanarak aşağıdaki gibi dotnet test komutunu çalıştırmanız yeterli olacaktır.
.NET Core - Unit Test Nedir Nasıl Yapılır Derinlemesine İnceleyelim

Ve böylece ilgili makalemizin sonuna gelmiş olduk. Uzunnn ve meşakkatli bir içerik olduğunun farkındayım. Direkt olarak ihtiyacınız doğrultusunda ilgilendiğiniz içeriğe odaklanabilmeniz için makalenin başındaki içerik klavuzu mahiyetinde oluşturduğum ‘İçindekiler’ listesinden istifade etmenizi tavsiye ederim…

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

7 Cevaplar

  1. Parviz dedi ki:

    emeginiz saglik Hocam , api baz alan test makalesi gelirmi ?

  2. Tarık dedi ki:

    Mükemmel

  3. Duygu dedi ki:

    Elinize sağlık, harika bir yazı. Ben çok faydalandım.

  4. mücteba dedi ki:

    (çok açıklayıcı bir makale, elinize sağlık)
    hocam biraz temel bir soru ama kafama takıldı:
    Mathematics mathematics = new(); ile Mathematics mathematics = new Mathematics (); arasındaki fark nedir?

  5. mücteba dedi ki:

    hocam c# 9.0 ile gelmiş. sizin blogda buldum. saygılar

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir