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, 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:
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# 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:
xUnit.net:
Microsoft.VisualStudio.TestTools.UnitTesting (MSTest):
Moq:
FluentAssertions:
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, 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:
Test Projesi Oluşturma:

dotnet new xunit -n MyUnitTestProject
xUnit Runner’ı Yükleme (Opsiyonel):
dotnet test
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

Visual Studio Test Explorer ile Testleri Çalıştırma:
.NET CLI ile Testleri Çalıştırma:
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.
[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);
}
[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);
}
[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);
}
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 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
Calculator calculator = new Calculator(); // Hesap makinesi oluştur
int number1 = 5;
int number2 = 10;
// Act
int result = calculator.Add(number1, number2); // Toplama işlemini gerçekleştir
// Assert
Assert.Equal(15, result); // Toplama işleminin sonucunun 15 olduğunu doğrula
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:
Assert.Equal(expected, actual);
Assert.NotEqual(notExpected, actual);
Assert.True(condition);
Assert.False(!condition);
Assert.Null(obj);
Assert.NotNull(obj);
Assert.Empty(collection);
Assert.NotEmpty(collection);
Assert.Contains(expectedItem, collection);
Assert.Throws<ExceptionType>(() => MethodUnderTest());
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:
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
}
}
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:
CalculatorTests veya DatabaseIntegrationTests gibi.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:
Kısaca,
TDD’yi projenize entegre ederken ve bu adımları izleyerek, projenizin gelişim sürecini daha verimli, güvenilir ve bakımlı hale getirebilirsiniz.