xUnit ile .NET Core Projelerinde Unit Test

Uzun bir süre NUnit ile birim testleri üzerine irili ufaklı çalışmalar gerçekleştirdim. Stabilitesi ve kullanım kolaylığı da hali hazırda NUnit’in popülerliğini korumaya devam etmesinin arkasındaki en önemli iki dinamiği.

Son dönemde ise — özellikle .NET Core’un desteği ile — xUnit büyük bir atılım içerisinde. Resmi web sitesinden de öğrenileceği üzere xUnit, aslında NUnit v2'nin geliştiricisi tarafından geliştirilen ve .NET Foundation’ın (herhangi bir kar amacı gütmeyen, yenilikçi ve açık kaynak kodlu .NET projelerinin yer aldığı topluluk) bir parçası olan bir birim test kütüphanesi.

Peki ama neden xUnit?

.NET uygulamalarında birim test yazarken aslında 3 büyük alternatif bize sunuluyor:

MSTest — NUnit — xUnit

Kütüphaneler birbirini teknolojik açıdan hızla takip etmeye başlamadan önce, seçim yapmak için çok keskin nedenler vardı. Örneğin, bir kütüphane çoklu parametre ile test fonksiyonu sunmazken, diğerinde bu özellik vardı ve sadece tek bir attribute ile bu iş çözülebiliyordu. Ya da bir kütüphane, kullandığınız IDE ile direkt entegre çalışıyorken ve raporları kullanıcı dostu bir şekilde görebiliyorken, diğeri için bir/birden fazla eklenti ile bu işi çözmeniz gerekiyordu.

Şimdi ise yukarıda ismi geçen 3 kütüphane de hemen hemen benzer özellikleri sunuyor. xUnit’in ilgili bağlantısında da bununla ilgili güzel bir karşılaştırma mevcut:

Kaynak: https://xunit.github.io/docs/comparisons.html

Benim kişisel olarak xUnit’e ilgili odağımı yönlendirmemin ilk sebebi merak :) NUnit’te yapabildiklerimi yapabiliyor mu sorusuna cevap aradım ve aradığım cevabı buldum. Sonrasında ise diğerlerinden farklı davranışları (özellikle Setup, TearDown, OneTimeSetup, OneTimeTearDown gibi alışagelmiş davranışları kökten değiştirmesi) kütüphaneyi eşelemek için yeterli bir sebepti.

Memnun muyum?

Şu ana kadar hiç bir engelleyici sorunla karşılaşmadım, “değilim” demek haksızlık olur. Hem Visual Studio’nun hem de Resharper’ın test ortamı ile sorunsuz bir şekilde entegre çalışabiliyor. Yazım kolaylığı ve genişletebilirlik özelliği (testleri istediğim herhangi bir custom attribute değerine göre önceliklendirme isteğim) ile takdire şayan.

O halde neden denemeyelim?

Soldaki gibi bir proje hiyerarşisi örneklerimizi çalıştırmak için yeterli duruyor.

framework, yardımcı fonksiyonlarımızın yer aldığı sınıfları üzerinde barındıran bir Common/Library (dotnet new classlib) projesi.

test, birim testlerimizin yer aldığı bir Test/xUnit (dotnet new xunit) projesi.

İlk olarak DateTimeExtensions.cs dosyasının içeriğine göz atmakta fayda var çünkü amacımız bu dosyadaki bir fonksiyona birim testler uygulamak:

ToPrettyDate fonksiyonu bir extension fonksiyon ve geriye, iliştirildiği tarih nesnesini GÜN — METİNSEL AY — YIL olarak geri döndürmekle sorumlu.

Şimdi sırasıyla testlerimizi yazmaya ve xUnit’in sunduğu test olanaklarını incelemeye başlayabiliriz. Bütün testlerimizi DateTimeExtensionsFixture.cs dosyası içerisinde barındıracağız.

İlk test

Amaç, fonksiyona bir kültür bilgisi atanmadığında hata fırlattığını, fırlatılan hatanın tipinin ArgumentNullException olduğunu ve hata mesajındaki parametre isminin doğru geçilip geçilmediğini kontrol etmek.

Naming Convention olarak birim testlerde genellikle;

FonksiyonAdı_BeklenenDeger_GecilecekParametre

tercih ediyorum. Bununla ilgili globalde kabul edilen 6–7 yöntem mevcut.

İlk satırdaki [Fact] attribute’ü, ilgili fonksiyonun bir test metodu olduğunu belirtmek üzere kullanıdığımız nesne. Fonksiyon içerisindeki 4. satırda kullandığımız Record nesnesi xUnit ile birlikte geliyor ve test edilen fonksiyondan fırlatılacak olan hatayı kayıt edebilmek için kullanılıyor. Biz de bu özelliği kullanarak, fırlatılan hatanın “ArgumentNullException” tipinde olup olmadığını (6. satır) ve hatadaki parametre isminin “culture” olup olmadığını (8. satır) bir test koşulu olarak kullanabiliyoruz.

Peki, elimden bir veri seti olsa ve hepsini sırasıyla teste sokmak istesem? Her biri için ayrı test fonksiyonu mu yazmam gerekiyor? Çok mantıklı değil elbette. İşte tam bu noktada xUnit, 3 attribute ile bizi karşılıyor.

InlineData, ClassData, MemberData

2., 3. ve 4. testlerimiz bu attribute’ler ile sarmalanacağı için öncesinde açıklamakta fayda görüyorum.

Bu attribute’leri Fact ile kullanamıyoruz, bunlara özel tasarlanmış Theory isimli attribute bize eşlik edecek. Theory olarak işaretlenmiş test fonksiyonları, geçilen parametre kadar çalıştırılacaktır.

InlineData, test metoduna parametreleri inline olarak geçmek amacıyla kullanılıyor. Inline’dan kasıt, attribute’e parametre olarak geçmek. Yani sizin test fonksiyonunuz 2 parametre ile çalışıyorsa, attribute’e de 2 parametre geçmeniz gerekli.

ToPrettyDate_ShouldAssertTrue_WhenCultureIsGerman test fonksiyonu çalışmak için 3 parametreye ihtiyaç duyuyor: Kültür bilgisi, mevcut tarih ve extensiondan gelecek cevabı karşılayacak olan değer. Fonksiyona kaç adet InlineData bağlarsak, test fonksiyonu o kadar çalışacaktır.

Bu noktada akla gelen ilk soru şu olsa gerek: İlk parametreye CultureInfo, ikinci parametreye ise DateTime geçemez miyiz? Ne yazık ki hayır. InlineData, parametre olarak primitive tipleri kabul ediyor.

Peki kompleks tipleri parametre olarak geçmek istersek?

Bu tipten türemiş objeleri test etmek için ClassData ya da MemberData attribute’lerini kullanacağız.

MemberData ile statik olarak tanımlanmış olan bir property, field ya da metodu test fonksiyonuna parametre olarak geçebiliriz. Buradaki atlanmaması gereken iki mutlak koşul:

  • Ögeler mutlaka statik olmalı!
  • Ögelerin dönüş değeri mutlaka IEnumerable<object[]> tipine dönüşebilir olmalı!

O zaman test sınıfımıza statik bir property tanımlayalım:

Property List<CultureTestParameter[]> dönüyor (2. koşula uygun) ve statik (1. koşula uygun). O halde test fonksiyonuna parametre olarak geçilebilir:

Statik property’ye istenildiği kadar CultureTestParameter tipinde değer eklenebilir ancak hem okunabilirlik olarak hem de test fonksiyonları ile test verilerini ayrıştırma olarak çok içime sinmediğini söyleyebilirim. Eğer sizin de içinize sinmediyse ClassData “ben de buradayım!” diyor.

ClassData ile TheoryData sınıfından kalıtım alan bir sınıfı test fonksiyonunuza parametre olarak geçebilirsiniz. Aslında TheoryData sınıfı da içerisinde IEnumerable<object[]> tipinde bir koleksiyon barındırıyor ve ekleme operasyonunu Add fonksiyonu ile hazır olarak sağlıyor.

12. ve 19. satırdaki Add(), temel sınıftan kalıtım ile geliyor. Bunun için 8. satırdaki kalıtım deklarasyonunu yapmanız yeterli. Son durumda test, şu şekilde oluyor:

Test fonksiyonu, CultureTestTheoryData sınıfının içerisindeki koleksiyona eklenen kayıt kadar çalışacaktır.

Yazıda bahsettiğim Setup, TearDown gibi özelliklere değinmediğimin farkındayım. Bunun için farklı bir örneğim hazır, yazıyı da çok kısa sürede hazırlayıp sunmayı planlıyorum.

Projenin tamamına aşağıdaki Github repo linkinden ulaşabilirsiniz. Bir sonraki yazıya kadar bütün birim testlerinizin yeşil oklarla şenlenmesi dileğiyle :)