İçeriğe geç
Educora
İleri21 dk12 / 16

xUnit ile test yazma

xUnit ile birim testleri yaz: `[Fact]`, `[Theory]` ve `[InlineData]`, doğrulamalar, FluentAssertions tarzı ve Moq ile bağımlılıkları taklit etme.

Kendini test et
Bu derste öğreneceklerin
  • [Fact] ve Assert metotlarıyla bir test sınıfı yazmak
  • [Theory] ve [InlineData] ile tek bir testi birçok veriyle çalıştırmak
  • Moq ile bir arayüzü taklit etmek ve çağrıları doğrulamak

Java kursunda JUnit ile tanıştıysan burada pek çok şey tanıdık gelecek: C#'ta da testler, kodu çağıran ve sonucu beklenen değerle karşılaştıran küçük metotlardır. .NET dünyasında en popüler test çatıları xUnit, NUnit ve MSTest'tir; dotnet new xunit şablonu ve ASP.NET Core'un kendi kaynak kodu xUnit kullanır. Önce bir testin fikrini hiçbir kütüphane olmadan görelim:

C#
(int Score, string Expected)[] cases = [(95, "A"), (80, "B"), (72, "C"), (49, "F")];
int passed = 0;

foreach (var (score, expected) in cases)
{
    string actual = Letter(score);
    bool ok = actual == expected;
    if (ok) passed++;
    Console.WriteLine($"{(ok ? "PASS" : "FAIL")} Letter({score}) = {actual}, expected {expected}");
}
Console.WriteLine($"{passed} of {cases.Length} passed");

// a bug hides here: > 80 instead of >= 80
static string Letter(int score) => score switch
{
    >= 90 => "A",
    > 80 => "B",
    >= 70 => "C",
    >= 50 => "D",
    _ => "F"
};
Beklenen çıktı
PASS Letter(95) = A, expected A
FAIL Letter(80) = C, expected B
PASS Letter(72) = C, expected C
PASS Letter(49) = F, expected F
3 of 4 passed
Testin özü: tek kontrol, çok veri; sınırdaki hata hemen ortaya çıkar

Test projesi ve [Fact]

Önceki derste dotnet new xunit -n Gradebook.Tests ile bir test projesi oluşturmuştuk. .NET 10 şablonu xunit, xunit.runner.visualstudio ve Microsoft.NET.Test.Sdk paketlerini ekler ve Xunit ad alanını genel bir using yapar. Test sınıfı, hiçbir öznitelik gerektirmeyen sıradan bir public sınıftır. Parametresiz bir test metodu [Fact] ile işaretlenir: “her zaman doğru olan bir olgu”. Adlar genellikle Method_Scenario_Expected kalıbıyla yazılır; testin kendisi ise AAA (Arrange, Act, Assert) yapısındadır.

C#
using Gradebook.Core;

namespace Gradebook.Tests;

public class GradeCalculatorTests
{
    [Fact]
    public void Average_OfThreeScores_ReturnsMean()
    {
        int[] scores = [85, 90, 95];                          // Arrange
        double result = GradeCalculator.Average(scores);      // Act
        Assert.Equal(90.0, result, precision: 3);             // Assert
    }

    [Fact]
    public void Average_OfEmptyList_Throws()
    {
        var ex = Assert.Throws<ArgumentException>(() => GradeCalculator.Average([]));
        Assert.Equal("No scores", ex.Message);
    }

    [Theory]
    [InlineData(95, "A")]
    [InlineData(80, "B")]
    [InlineData(72, "C")]
    [InlineData(49, "F")]
    public void Letter_ReturnsExpectedGrade(int score, string expected)
    {
        Assert.Equal(expected, GradeCalculator.Letter(score));
    }
}
Gradebook.Tests/GradeCalculatorTests.cs — GradeCalculator sınıfı önceki derstendir

Assert.Equal(expected, actual) temel doğrulamadır; ondalıklı sayılar için precision ondalık basamak sayısını belirler. Assert.Throws<T> istisnanın türünü doğrular ve onu döndürür. Diğer yararlı metotlar: Assert.True, Assert.Null, Assert.Contains, Assert.Empty, Assert.InRange. xUnit her test için sınıfın yeni bir örneğini oluşturur: hazırlık kodu yapıcıya, temizlik ise IDisposable.Dispose() metoduna yazılır; ayrı [SetUp] öznitelikleri yoktur.

JUnit 5xUnitAnlamı
@Test[Fact]tek bir test
@ParameterizedTest + @CsvSource[Theory] + [InlineData]veriye dayalı test
@BeforeEach / @AfterEachyapıcı / Dispose()her testten önce / sonra
@BeforeAllIClassFixture<T>tüm sınıf için ortak hazırlık
assertThrowsAssert.Throws<T>istisna doğrulama

Testleri dotnet test çalıştırır (Visual Studio ve Rider'da ise Test Explorer penceresi). Diyelim ki yukarıdaki örnekteki gibi Letter metodunda >= 80 yanlışlıkla > 80 oldu:

Terminal
dotnet test
Beklenen çıktı
Failed Gradebook.Tests.GradeCalculatorTests.Letter_ReturnsExpectedGrade(score: 80, expected: "B") [2 ms]
  Error Message:
   Assert.Equal() Failure: Strings differ
Expected: "B"
Actual:   "C"

Failed!  - Failed:     1, Passed:     5, Skipped:     0, Total:     6, Duration: 41 ms - Gradebook.Tests.dll (net10.0)
Örnek çıktı (kısaltılmış): [InlineData(80, "B")] durumu hatayı yakaladı

[Theory] ve test verileri

[Theory] tek bir testi farklı verilerle çalıştırır: her [InlineData] özniteliği ayrı bir test durumudur ve raporda kendi parametreleriyle görünür; yukarıda toplam altı test durumu vardı. Veri bir özniteliğe sığmıyorsa (örneğin nesneler ya da listeler), [MemberData(nameof(Cases))] onu statik bir özellikten alır. Asenkron kodu test ederken ise metot mutlaka async Task döndürmelidir:

Yanlış: `async void`
[Fact]
public async void LoadsScores()          // async void: the runner cannot await it
{
    var scores = await repository.LoadAsync("Aysel");
    Assert.NotEmpty(scores);
}
Doğru: `async Task`
[Fact]
public async Task LoadsScores()          // async Task: xUnit awaits the result
{
    var scores = await repository.LoadAsync("Aysel");
    Assert.NotEmpty(scores);
}
Test çalıştırıcısı bir async void metodunu bekleyemez; bu yüzden hatası kaybolabilir ya da başka bir yerde ortaya çıkabilir

Moq ile bağımlılıkları taklit etmek

ReportService puanları IScoreRepository arayüzünden alır; gerçek programda onun arkasında bir veritabanı durur. Birim testinin veritabanına ihtiyacı yoktur: Moq kütüphanesi (dotnet add package Moq) arayüzün sahte bir nesnesini, yani bir mock oluşturur. Bağımlılık sınıfa yapıcıdan gelir; burada C# 12'nin birincil yapıcısıyla (primary constructor) yazıldı.

C#
public interface IScoreRepository
{
    IReadOnlyList<int> GetScores(string student);   // e.g. reads a database
}

public class ReportService(IScoreRepository repository)
{
    public string Summary(string student)
    {
        double average = GradeCalculator.Average(repository.GetScores(student));
        return $"{student}: {GradeCalculator.Letter((int)average)}";
    }
}
Servis somut bir veritabanını değil, arayüzü tanır
C#
using Moq;

public class ReportServiceTests
{
    [Fact]
    public void Summary_UsesScoresFromRepository()
    {
        var repo = new Mock<IScoreRepository>();
        repo.Setup(r => r.GetScores("Aysel")).Returns(new List<int> { 90, 80 });

        var service = new ReportService(repo.Object);
        string summary = service.Summary("Aysel");

        Assert.Equal("Aysel: B", summary);
        repo.Verify(r => r.GetScores("Aysel"), Times.Once);
    }
}
Moq ile test: sahte depo 90 ve 80 döndürür, ortalama 85, yani “B”

new Mock<IScoreRepository>() sahte nesneyi oluşturur, Setup(...).Returns(...) ona yanıtı öğretir, repo.Object ise servise verilen hazır örnektir. Verify(..., Times.Once) metodun tam bir kez çağrıldığını doğrular. Moq yalnızca arayüzleri ve virtual metotları taklit edebilir; bağımlılıkları arayüz olarak tanımlamanın bir nedeni de budur. Popüler bir alternatif kütüphane NSubstitute'tur.

FluentAssertions'ın fikri, doğrulamaları cümle gibi okunan zincirlere dönüştürmektir: summary.Should().Be(...). Hata mesajları da daha ayrıntılı olur. Dikkat: FluentAssertions 8. sürümden (2025) itibaren ticari projeler için ücretli lisans gerektirir; ücretsiz bir alternatif olarak örneğin Shouldly kütüphanesi vardır.

Klasik `Assert`
Assert.Equal("Aysel: B", summary);
Assert.Equal(3, scores.Count);
Assert.Contains(95, scores);
FluentAssertions tarzı
summary.Should().Be("Aysel: B");
scores.Should().HaveCount(3).And.Contain(95);

Neyi test etmeli? Test piramidi kuralı der ki: en çok hızlı birim testleri (tek sınıf, sahte bağımlılıklar), daha az entegrasyon testi (gerçek bir veritabanı ya da web sunucusuyla) ve en az, tüm sistemi sınayan arayüz testleri. Kodun ne kadarının testlerle kapsandığını şablondaki coverlet.collector paketi ölçer: dotnet test --collect:"XPlat Code Coverage" bir rapor dosyası oluşturur. Ama %100 kapsama bir amaç değildir; sınır durumları ve hataya açık yerler daha önemlidir.

Önemli noktalar

  • xUnit: [Fact] parametresiz bir test, [Theory] + [InlineData] ise veriye dayalı bir testtir.
  • Assert.Equal(expected, actual), Assert.Throws<T>, Assert.Contains temel doğrulamalardır.
  • Her test yeni bir nesnede çalışır: hazırlık yapıcıda, temizlik Dispose() içindedir.
  • Asenkron testler async Task döndürür, asla async void değil.
  • Moq: Setup(...).Returns(...) yanıtı öğretir, .Object sahte örneği verir, Verify çağrıyı doğrular.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Parametreleri [InlineData] özniteliğinden gelen test hangi öznitelikle işaretlenir?