Blog Kategorileri

C# Unit Test

Bu bölümde, C# ve .NET’te unit test kavramını açıklayacağız. unit testinin ne olduğunu, neden önemli olduğunu ve geliştiricilerin kullanabileceği test frameworklerini ve araçlarının yapısını ele alacağız.

Unit Test Nedir?

Unit test, bir yazılım uygulamasının en küçük işlevsel parçalarını (birimleri) test etmek için kullanılan bir test türüdür. Bu testler genellikle kodun belirli bir fonksiyonu, metodu veya sınıfı gibi küçük birimlerini izole eder ve bu birimlerin doğru şekilde çalışıp çalışmadığını doğrulamak için kullanılır.

Unit testlerin bazı önemli özellikleri şunlardır:

  1. İzole Testler: Unit testler, birimin etrafındaki diğer bileşenleri (diğer sınıflar, veritabanı bağlantıları, ağ çağrıları vb.) izole eder. Bu sayede test sonuçları, sadece test edilen birimin doğruluğunu gösterir ve dış etkenlerden etkilenmez.
  2. Tekrarlanabilirlik: Unit testler, kodunuzda yapılan değişikliklerin ardından kolayca tekrar çalıştırılabilir ve koddaki herhangi bir değişikliğin mevcut işlevselliği bozup bozmadığını hızlıca kontrol etmenizi sağlar.
  3. Hızlı Geri Bildirim: Unit testler genellikle hızlı bir şekilde çalıştırılır ve sonuçları hızlıca sağlar. Bu, yazılım geliştirme sürecinde hızlı geri bildirim almanızı ve hataları erken aşamada yakalamanızı sağlar.
  4. Dökümantasyon ve Tasarım: Unit testler, kodunuzun nasıl kullanılması gerektiği hakkında belgeleme sağlar. Ayrıca, kodunuzun doğru bir şekilde tasarlanıp tasarlanmadığını ve istenen işlevselliği sağladığını doğrulamanıza yardımcı olur.

Unit testler, yazılımın güvenilirliğini artırmak, hataları erken aşamada tespit etmek, kodun bakımını kolaylaştırmak ve kod kalitesini artırmak için yaygın olarak kullanılan bir geliştirme pratiğidir. Birçok modern yazılım geliştirme süreci ve metodolojisi (örneğin, TDD – Test Driven Development) unit testleri önemli bir parça olarak kabul eder.

C# ve .NET Unit Test Framework ve Araçları

C# dilinde unit testleri yazmak için birçok farklı framework ve araç bulunmaktadır. İşte C# için yaygın olarak kullanılan unit test frameworkleri ve araçları:

NUnit:

  • NUnit, C# için popüler ve güçlü bir unit test framework’üdür.
  • Özellikle .NET Framework platformuyla entegrasyonu ve geniş topluluk desteğiyle bilinir.

xUnit.net:

  • xUnit.net, açık kaynaklı bir unit test framework’üdür ve diğer diller ve platformlar için de kullanılabilir. .NET Core’da unit testi için genellikle tercih edilir.
  • Yalın bir tasarıma sahiptir ve modern C# geliştirme standartlarına uygun olarak geliştirilmiştir.

Microsoft.VisualStudio.TestTools.UnitTesting (MSTest):

  • MSTest, Visual Studio içinde bulunan bir unit test framework’üdür.
  • Visual Studio ile sıkı entegrasyonu sayesinde kolay kullanım sağlar.

Moq:

  • Moq, C# için bir mock framework’üdür ve genellikle unit testlerde kullanılan sahte nesnelerin (mocks) oluşturulması için kullanılır.
  • Gerçek nesnelerin yerine geçebilen ve testlerde kullanımı kolay sahte nesneler oluşturmanıza yardımcı olur.

FluentAssertions:

  • FluentAssertions, C# unit testleri için bir assertion library’sidir.
  • Doğru sonuçların elde edildiğini doğrulamak için kullanılır ve okunabilir bir yapıya sahiptir.
  • FluentAssertions, özellikle karmaşık veri yapıları veya nesneler üzerinde doğrulama yaparken kullanışlıdır.

Bu framework’ler ve araçlar, C# ile yazılan yazılımların test edilmesi ve kalitesinin artırılması için kullanılan güçlü ve yaygın olarak kabul edilmiş araçlardır. Her framework’ün güçlü ve zayıf yönleri vardır ancak bu makalede, unit test için C# geliştiricileri arasında popüler olan xUnit ve Moq’a odaklanacağız.

xUnit Nedir?

xUnit, C# ve .NET platformu için geliştirilmiş açık kaynaklı bir unit test framework’üdür. xUnit.net, diğer xUnit türevleri gibi birçok programlama diline ve platforma genişletilebilir bir yapı sunar. Bu framework, modern C# geliştirme standartlarına uygun olarak tasarlanmış ve kullanımı kolaydır.

İşte xUnit’in bazı temel özellikleri:

  1. Yalın Tasarım: xUnit.net, yalın bir tasarıma sahiptir ve kullanımı kolaydır. Testlerin tanımlanması, çalıştırılması ve sonuçların analiz edilmesi oldukça basittir.
  2. Veri Teorisi ile Testler: xUnit, veri teorisi özelliği sayesinde aynı testi farklı veri setleriyle tekrarlayarak kodunuzu geniş kapsamlı test edebilmenize olanak tanır.
  3. Paralel Test Çalıştırma: xUnit, testleri paralel olarak çalıştırabilme yeteneği sunar. Bu özellik, test sürelerini kısaltmaya ve testlerin daha hızlı bir şekilde tamamlanmasına yardımcı olur.
  4. Özel Test Koşulları: xUnit, testlerin belirli koşullara göre çalıştırılmasını sağlayan özel test koşulları (test attributes) sunar. Örneğin, sadece belirli bir platformda veya ortamda bir testin çalışmasını isteyebilirsiniz.
  5. Zengin Assertion Kütüphanesi: xUnit, assert işlemlerini gerçekleştirmek için zengin bir assertion kütüphanesi sunar. Bu kütüphane sayesinde test sonuçlarını doğrulamak ve beklenen davranışları kontrol etmek kolaylaşır.
  6. Entegrasyon: Visual Studio, VSCode, ReSharper ve .NET CLI gibi popüler araçlarla entegrasyonlara sahiptir ve test deneyimini kolaylaştırır.

.NET Projesinde xUnit Kurulumu

Test Projesi Oluşturma:

  • Visual Studio kullanıyorsanız, “Solution Explorer” panelinde sağ tıklayıp “Add > New Project” seçeneğini seçin. “Search” sekmesine geçin ve “xunit” araması yaparak xUnit Test Projesini projenize ekleyin.
  • .NET CLI kullanıyorsanız, terminal veya komut istemcisinde projenizin olduğu dizine gidin ve aşağıdaki komutu çalıştırın:
dotnet new xunit -n MyUnitTestProject

xUnit Runner’ı Yükleme (Opsiyonel):

  • Testleri çalıştırmak için xUnit Runner’ı yükleyebilirsiniz. Bu, testleri GUI veya komut satırından çalıştırmanıza olanak tanır.
  • Visual Studio kullanıyorsanız, Test menüsünden “Test Explorer”ı açın ve “Run All” seçeneğini kullanarak testleri çalıştırabilirsiniz.
  • .NET CLI kullanıyorsanız, terminal veya komut istemcisinde aşağıdaki komutu kullanarak testleri çalıştırabilirsiniz:
dotnet test

xUnit ile C# Unit Test Senaryosu

Bu bölümde xUnit kullanarak basit bir unit testi oluşturmayı anlatacağız. Örneğimiz için, bir Hesap Makinesi sınıfı içinde bir topla ve fark metodunun aşağıdaki uygulamasına sahip olduğumuzu varsayalım:

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Subtract(int a, int b)
    {
        //Operasyon yanlış.
        return a + b;
    }
}

Fark (Subtract) metodunda bilinçli olarak hata yapılmıştırç çkarma işlemi yerine toplama işlemi yapılmıştır.

Add ve Subtract metodlarına yönelik bir unit testi yazmak için oluşturduğumuz MyUnitTestProject projemizde CalculatorTests adlı yeni bir test sınıfı oluşturun. Bu sınıfın içine, aşağıdaki gibi, [Fact] attribute ile işaretlenmiş AddTest ve SubtractTest adlı metodlar ekleyin:

    public class CalculatorTests
    {
        [Fact]
        public void AddTest()
        {
            // Arrange
            var calculator = new Calculator();
            int a = 3;
            int b = 5;
            int expectedResult = 8; //Beklenen sonuç

            // Act
            int actualResult = calculator.Add(a, b);

            // Assert
            Assert.Equal(expectedResult, actualResult);
        }

        [Fact]
        public void SubtractTest()
        {
            // Arrange
            var calculator = new Calculator();
            int a = 7;
            int b = 2;
            int expectedResult = 5; //Beklenen sonuç

            // Act
            int actualResult = calculator.Subtract(a, b);

            // Assert
            Assert.Equal(expectedResult, actualResult);
        }
    }

Not: MyUnitTestProject’e BasicMaths projesinin referansını vermeyi unutmayın.

Projenin son durumu

Testi Çalıştırma

Visual Studio Test Explorer ile Testleri Çalıştırma:

  • Visual Studio kullanıyorsanız, Test Explorer panelini kullanarak testleri kolayca çalıştırabilirsiniz.
  • Visual Studio’da Test menüsünden “Test Explorer”ı açın.
  • Test Explorer panelinde “Run All” veya “Run Selected Tests” seçeneklerini kullanarak tüm testleri veya seçilen testleri çalıştırabilirsiniz.

.NET CLI ile Testleri Çalıştırma:

  • .NET CLI (Command Line Interface) kullanarak terminal veya komut istemcisinden testleri çalıştırabilirsiniz.
  • Projenizin kök dizinine gidin ve aşağıdaki komutu kullanarak testleri çalıştırın:
dotnet test

Artık test sonuçlarını dikkatlice inceleyerek olası hataları ve başarı durumlarını analiz edebilirsiniz.

Kendi test analizimizi yapacak olursak Add metodunun başarılı bir şekilde çalıştığını Subtract’in ise başarısız olduğunu gözlemledik. Çünkü biz 7 den 2 çıkarsa 5 gelmesi gerektiğini teste belirttik lakin biz subtract metodumuzda çıkarma yerine bir hata yaparak toplama işlemi yapmıştık o nedenle testen geçemedik. Şimdi bu test sonucumuza göre metodumuza geri dönüp yapmış olduğumuz hatamızı düzeltebiliriz.

xUnit Test Attribute’leri (Özellikleri)

  • [Fact] niteliği, bir test metodu olduğunu belirtir. Bu nitelikle işaretlenen metotlar, xUnit test framework’ü tarafından otomatik olarak algılanır ve çalıştırılır.
[Fact]
public void TestMethod()
{
    // Test assertions
}
  • [Theory] niteliği, parametreli testler oluşturmanıza olanak tanır. Bu nitelikle işaretlenen metotlar, farklı veri setleriyle birden fazla kez çalıştırılabilir.
  • [InlineData] niteliği, [Theory] niteliğiyle birlikte kullanılır ve parametreli testlerde kullanılacak veri setini belirtir.
[Theory]
[InlineData(2, 3, 5)]
[InlineData(5, 5, 10)]
public void TestAddition(int a, int b, int expectedResult)
{
    var result = Calculator.Add(a, b);
    Assert.Equal(expectedResult, result);
}
  • [InlineData with MemberData]: [MemberData] niteliği, [Theory] niteliğiyle birlikte kullanılarak parametreli testlerde farklı veri setlerini dinamik olarak sağlar.
public static IEnumerable<object[]> TestData()
{
    yield return new object[] { 2, 3, 5 };
    yield return new object[] { 5, 5, 10 };
}

[Theory]
[MemberData(nameof(TestData))]
public void TestAddition(int a, int b, int expectedResult)
{
    var result = Calculator.Add(a, b);
    Assert.Equal(expectedResult, result);
}
  • [InlineData with ClassData]: [ClassData] niteliği, [Theory] niteliğiyle birlikte kullanılarak farklı veri setlerini bir sınıftan sağlar. Bu yöntemle veri setlerini daha düzenli ve yönetilebilir hale getirebilirsiniz.
public class TestDataClass : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 2, 3, 5 };
        yield return new object[] { 5, 5, 10 };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[Theory]
[ClassData(typeof(TestDataClass))]
public void TestAddition(int a, int b, int expectedResult)
{
    var result = Calculator.Add(a, b);
    Assert.Equal(expectedResult, result);
}

Etkili ve Sürdürülebilir C# Unit Testleri Yazma

Unit testlerin temel bir yönü, anlaşılması, bakımı ve genişletilmesi kolay olan testler oluşturmaktır. Bu bölümde, iyi yapılandırılmış bir testin anatomisini inceleyecek, test tasarımında SOLID prensiplerini uygulayacak, test kurulumunu ve sonlandırmasını kapsayacak ve daha fazlasını ele alacağız.

İyi yapılandırılmış bir testin anatomisi: Arrange (Düzenle), Act (Eylem), Assert (Doğrula)

İyi bir unit testi, kodu basit, anlaşılabilir ve bakıma uygun hale getiren “Arrange, Act, Assert” desenini takip eder. Bu deseni açıklamak için şu adımları inceleyelim:

  • Arrange: İlk olarak, test ortamını hazırlayarak başlarız. Bu aşamada, hesap makinesi sınıfını oluştururuz ve toplama işlemi için gereken parametreleri ayarlarız. Örneğin:
// Arrange
Calculator calculator = new Calculator(); // Hesap makinesi oluştur
int number1 = 5;
int number2 = 10;
  • Act: Hazırlanan ortamı kullanarak, toplama işlemini gerçekleştiririz. Örneğin:
// Act
int result = calculator.Add(number1, number2); // Toplama işlemini gerçekleştir
  • Assert: Şimdi, beklenen sonucun gerçek sonuçla eşleşip eşleşmediğini doğrularız. Toplama işleminin doğru çalıştığından emin olmak için bir doğrulama yaparız. Örneğin:
// Assert
Assert.Equal(15, result); // Toplama işleminin sonucunun 15 olduğunu doğrula

Yaygın Kullanılan Assert Metotları

Programlama dilleri ve test çerçeveleri arasında farklılık gösterse de, genel olarak yaygın olarak kullanılan assert metotlarının bazı özellikleri şunlardır:

  • Equal: İki değerin eşit olup olmadığını kontrol eder. Genellikle primitive tipler veya nesne referanslarının karşılaştırılmasında kullanılır.
Assert.Equal(expected, actual);
  • NotEqual: İki değerin eşit olmadığını kontrol eder.
Assert.NotEqual(notExpected, actual);
  • True ve False: Bir koşulun doğru veya yanlış olduğunu kontrol eder.
Assert.True(condition);
Assert.False(!condition);
  • Null ve NotNull: Bir nesnenin null olup olmadığını veya null olmadığını kontrol eder.
Assert.Null(obj);
Assert.NotNull(obj);
  • Empty ve NotEmpty: Bir koleksiyonun veya dize gibi bir veri yapısının boş olup olmadığını kontrol eder.
Assert.Empty(collection);
Assert.NotEmpty(collection);
  • Contains: Bir koleksiyonun belirli bir öğeyi içerip içermediğini kontrol eder.
Assert.Contains(expectedItem, collection);
  • Throws: Belirli bir koşul altında bir istisna fırlatılıp fırlatılmadığını kontrol eder.
Assert.Throws<ExceptionType>(() => MethodUnderTest());

Test Tasarımında SOLID Prensiplerini Uygula

Unit testlerin yönetilebilir, bakıma uygun ve anlaşılır kalmasını sağlamak için, üretim kodunda olduğu gibi, test aşamasındada SOLID prensiplerine uyulmalıdır:

  • Tekil Sorumluluk Prensibi (SRP): Her test, belirli bir birimi veya davranışı odaklanmalıdır. Tek bir test içinde birden fazla doğrulama yapmaktan kaçının, bu sayede testi daha anlaşılır ve sorun gidermeyi daha kolay hale getirir.
  • Açık/Kapalı Prensibi (OCP): Testlerin genişletilebilir olmasını sağlayın, yani yeni test senaryoları eklemek mevcut olanları değiştirmeyi gerektirmemelidir.
  • Liskov’un Yerine Geçme Prensibi (LSP): Test mirası veya paylaşılan yapılar kullanırken, temel sınıfların veya yapıların türetilmiş tipleriyle değiştirilebilir olmasına dikkat edin ve test bütünlüğünü bozmadan değiştirilebilir olmalıdır.
  • Interface Ayırma Prensibi (ISP): Bir test belirli bir interface gerektiriyorsa, yalnızca o interface bağlı olmalıdır, daha büyük ve karmaşık bir yapıya bağlı olmamalıdır. Bu, bağımlılıkları ve testin kapsamını daraltmaya yardımcı olur.
  • Bağımlılıkların Ters Çevrilmesi Prensibi (DIP): Somut uygulamalar yerine soyutlamalara bağımlılık oluşturun. Testlerde, bu, Moq gibi mocklama framework’lerini kullanarak testleri bağımlılıkların gerçek uygulamalarından izole etmek anlamına gelir.

Encapsulate Test Setup ve Teardown

Test kurulumunu ve sonlandırmasını kapsüllü hale getirmek, unit testlerin daha düzenli, okunabilir ve bakımı kolay hale gelmesini sağlar. Bu, testlerinizi daha modüler hale getirir ve tekrar kullanılabilirliklerini artırır. İşte test kurulumunu ve sonlandırmasını kapsüllenmesi için bazı yaklaşımlar:

[Setup] ve [Teardown] Nitelikleri: xUnit ve diğer bazı test çerçeveleri, test kurulumu ve sonlandırması için [Setup] ve [Teardown] niteliklerini destekler. Bu nitelikleri kullanarak, her test metodu öncesi ve sonrası çalışacak kod bloklarını tanımlayabilirsiniz.

public class MyTestClass
{
    private DatabaseContext _dbContext;

    [Setup]
    public void Setup()
    {
        _dbContext = new DatabaseContext();
        // İlgili diğer hazırlıklar
    }

    [Teardown]
    public void Teardown()
    {
        _dbContext.Dispose();
        // İlgili diğer temizlik işlemleri
    }

    [Fact]
    public void TestMethod1()
    {
        // Test assertions
    }
}

[Setup] niteliğiyle test öncesi hazırlıkları, [Teardown] niteliğiyle ise test sonrası temizlik işlemleri yapılır.

TestFixture ve IDisposable Kullanımı: Bazı test çerçeveleri, test sınıfları için özel bir “TestFixture” veya “TestContext” sağlar. Bu sınıfları IDisposable arayüzüyle kullanarak, testlerin öncesi ve sonrası işlemlerini daha kontrol edilebilir bir şekilde yapabilirsiniz.

public class MyTestFixture : IDisposable
{
    private DatabaseContext _dbContext;

    public MyTestFixture()
    {
        _dbContext = new DatabaseContext();
        // İlgili diğer hazırlıklar
    }

    public void Dispose()
    {
        _dbContext.Dispose();
        // İlgili diğer temizlik işlemleri
    }
}

public class MyTestClass : IClassFixture<MyTestFixture>
{
    private readonly MyTestFixture _fixture;

    public MyTestClass(MyTestFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void TestMethod1()
    {
        // Test assertions
    }
}

Bu örnekte, MyTestFixture sınıfı IDisposable arayüzünü kullanarak test öncesi ve sonrası işlemleri gerçekleştirir. MyTestClass ise IClassFixture arayüzünü kullanarak MyTestFixture‘ı kullanır.

Bu yöntemler, testlerinizi daha düzenli ve modüler hale getirerek, test kurulumu ve sonlandırmasını kapsüllenmesini sağlar ve testlerinizin bakımını kolaylaştırır.

IAsyncLifetime Kullanımı: asenkron testler için daha geniş kapsamlı ve esnek bir test kurulumu ve sonlandırma işlemi sağlar. InitializeAsync: Test sınıfının bir örneği oluşturulduğunda, bu metot çağrılır ve test öncesi hazırlıkları yapılır. DisposeAsync: Test sınıfının örneği artık kullanılmayacak hale geldiğinde, bu metot çağrılır ve test sonrası temizlik işlemleri yapılır. Bu metotlar async Task döndürmelidir.

public class MyTestClass : IAsyncLifetime
{
    private DatabaseContext _dbContext;

    public async Task InitializeAsync()
    {
        _dbContext = new DatabaseContext();
        // İlgili diğer hazırlıklar
        await Task.Delay(100); // Örnek olarak asenkron bir işlem
    }

    public async Task DisposeAsync()
    {
        _dbContext.Dispose();
        // İlgili diğer temizlik işlemleri
        await Task.Delay(100); // Örnek olarak asenkron bir işlem
    }

    [Fact]
    public void TestMethod1()
    {
        // Test assertions
    }
}

Unit Test Best Practices Yaklaşımlar

Temiz ve bakımlı test kodunu sürdürmek için, organizasyon ve isimlendirme kurallarına dikkat etmek çok önemlidir. Bazı en iyi uygulamalar şunları içerir:

  • İsimlendirme: Test metodlarına açıklayıcı başlıklar verin ki testin amacı hızlıca anlaşılabilsin. MethodName_Scenario_ExpectedBehavior gibi bir kural kullanmak, testin niyetini hızlıca anlamayı sağlar.
  • Organizasyon: İlgili testleri aynı sınıf veya isim uzayında gruplayın, böylece ilgili testleri bulmak daha kolay olur. Örneğin, CalculatorTests veya DatabaseIntegrationTests gibi.
  • Granülerlik: Her test vakasında tek bir davranışı test etmeyi hedefleyin. Testlerinizi çok geniş veya karmaşık hale getirmek yerine, küçük ve belirli davranışları test eden testler yazmaya özen gösterin.
  • Okunabilirlik: Temiz ve anlaşılır testler yazın, böylece geliştiricilerin bir testin niyetini ve beklentilerini anlaması kolaylaşır.

Test-Driven Development (Test Odaklı Geliştirmeyi) (TDD) Dahil Etme

TDD (Test-Driven Development), yazılım geliştirme sürecinde kullanılan bir metodolojidir. Bu metodoloji, yazılımın test edilebilirliğini artırmak ve kod kalitesini yükseltmek amacıyla kullanılır. Temel prensibi, yazılım geliştirme sürecinde testlerin önce yazılması ve ardından kodun bu testleri geçecek şekilde uygulanmasıdır.

TDD’nin ana adımları şunlardır:

  • Test Yazma (Red): İlk adım olarak, yazılacak bir özellik veya işlev için bir test yazılır. Bu test, istenen işlevin henüz uygulanmadığı bir durumu kontrol eder. Bu aşamada test başarısız olur (kırmızı durum).
  • Kod Yazma (Green): İkinci adımda, testinizi geçecek kadar minimum kodu yazarsınız. Bu adımda hedef, testinizi başarılı hale getirmektir. Kodunuzun, testinizi geçecek şekilde çalışması için minimum işlevselliği sağlaması gerekir.
  • Refaktörasyon (Refactor): Üçüncü adımda, kodunuzu yeniden düzenleyerek ve iyileştirerek daha temiz, daha okunabilir ve daha sürdürülebilir hale getirirsiniz. Bu adım, kodunuzun kalitesini artırmak için kullanılır.

Kısaca,

  1. Başarısız bir test yazın.
  2. Testi geçecek şekilde işlevleri uygulayın.
  3. Testi başarılı bir şekilde geçirirken kodu yeniden düzenleyin.

TDD’yi projenize entegre ederken ve bu adımları izleyerek, projenizin gelişim sürecini daha verimli, güvenilir ve bakımlı hale getirebilirsiniz.

Last Updated : 30/04/2024

Kaliteli Test Senaryoları için Gelişmiş xUnit Teknikleri

2023 © Coding, Developed by alkanfatih.com