Mert Özen Her Satırda Daha İleri
Test ve Kalite Unit Test — #14

xUnit v3 Assert Rehberi: Equal, Equivalent, Collection, Throws, Raises, Skip ve Kendi Assertion Helper'ın

Mert Özen 15 Kas 2026 13 dk 3 görüntülenme
xUnit v3 Assert Rehberi: Equal, Equivalent, Collection, Throws, Raises, Skip ve Kendi Assertion Helper'ın

xUnit v3'ün yerleşik Assert'i günlük işlerin çoğunu ek paket olmadan karşılar; asıl iş doğru method'u seçmek ve varsayılanlarını bilmektir. Assert.Equivalent sırayı ve fazladan property'leri varsayılan olarak görmezden gelir, strict modda ise koleksiyon hatasını yanıltıcı bir mesajla raporlar. Equal'ın tolerance ve string seçenekleri, Collection, Throws/ThrowsAsync, Raises, Skip ve IAssertionException ile kendi helper'ın, gerçek hata mesajlarıyla.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3 ve xunit.analyzers 2.3.0 ile derlenip denenmiştir; testler Microsoft.Testing.Platform (MTP; .NET'in yeni test çalıştırma altyapısı) ile dotnet test üzerinden koşuldu.

Önce Tahmin Et

Bu yazı kavram anlatmıyor; xUnit v3'ün yerleşik Assert class'ını aile aile, her gün kullanılan method'larıyla geçiyor. Örnekler sitedeki serilerin ortak örnek uygulaması Bookstore'dan (bir online kitapçı): sepet toplamı, sipariş satırları, stok yetersizliği ve sipariş verildiğinde yayınlanan event. Önce bir soru. Sipariş iki satırlı: önce 2 adet Dune (12,50 EUR), sonra 1 adet The Hobbit (9,90 EUR). İki test aynı beklentiyi farklı sırayla ve fiyatsız yazıyor:

[Fact]
public void Lines_are_two_dune_and_one_hobbit()
{
    Order order = DuneAndHobbit();

    Assert.Equivalent(
        new[]
        {
            new { Isbn = Hobbit, Quantity = 1 },
            new { Isbn = Dune, Quantity = 2 },
        },
        order.Lines);
}

[Fact]
public void Lines_are_two_dune_and_one_hobbit_strict()
{
    Order order = DuneAndHobbit();

    Assert.Equivalent(
        new[]
        {
            new { Isbn = Hobbit, Quantity = 1 },
            new { Isbn = Dune, Quantity = 2 },
        },
        order.Lines,
        strict: true);
}

OrderLine bir record: OrderLine(string Isbn, int Quantity, decimal UnitPrice). Hangi test geçer? Kalan varsa hata mesajı sorunu doğru anlatır mı?

Cevap: Equivalent Fazladan Member'ı Görmezden Geliyor

İlk test geçiyor, ikincisi kalıyor:

failed Bookstore.Ordering.Tests.OrderLinesPredictTests.Lines_are_two_dune_and_one_hobbit_strict (3ms)
  Assert.Equivalent() Failure: Collection value not found
  Expected: { Isbn = "9780261103344", Quantity = 1 }
  In:       [OrderLine { Isbn = 9780441172719, Quantity = 2, UnitPrice = 12.50 }, OrderLine { Isbn = 9780261103344, Quantity = 1, UnitPrice = 9.90 }]

Assert.Equivalent iki şeyi varsayılan olarak esnek yapıyor. Koleksiyonlarda sıraya bakmıyor: beklenen her element için gerçek koleksiyonda eşdeğer bir element arıyor. Object'lerde de yalnız beklenen tarafta olan member'ları karşılaştırıyor; gerçek object'teki fazladan UnitPrice hiç kontrol edilmiyor. İlk test bu yüzden yeşil ve bu yüzden tehlikeli: fiyatı hiç doğrulamıyor, satıra yanlış fiyat yazan bir değişiklik onu kırmaz.

strict: true fazladan member'ı da hata sayıyor. Ama mesaja dikkat et: "Collection value not found" diyor ve The Hobbit satırını, gerçek koleksiyonda birebir aynı ISBN (International Standard Book Number; kitapların uluslararası kimlik numarası) ve adetle dururken gösteriyor. Sebep sıra değil, UnitPrice. Koleksiyon içinde strict karşılaştırma yapılınca xUnit hangi member'ın uyuşmadığını değil, eşini bulamadığı elementi raporluyor. Doğru düzeltme beklenen object'e UnitPrice'ı eklemek; bunu aşağıdaki Equivalent bölümünde yapıyoruz.

Eşitlik ve Değer Assertion'ları

Testlerin büyük kısmı bu aileyle yazılır. 03. yazıda okunabilir assertion'ın neden önemli olduğunu konuşmuştuk; burada her method'un ne zaman doğru seçim olduğuna bakalım:

[Fact]
public void Subtotal_is_the_sum_of_line_totals()
{
    Cart cart = new Cart()
        .Add(Dune, "Dune", 12.50m, quantity: 2)
        .Add(Hobbit, "The Hobbit", 9.90m);

    Assert.Equal(34.90m, cart.Subtotal);
}

[Fact]
public void Institutional_discount_is_rounded_to_cents()
{
    // 3 x 12,45 = 37,35; %10 indirimle 33,615 -> 33,62
    Cart cart = new Cart().Add(Dune, "Dune", 12.45m, quantity: 3);

    Assert.Equal(33.62m, cart.Total(discountRate: 0.10m));
}

[Fact]
public void Discounted_total_never_exceeds_the_subtotal()
{
    Cart cart = new Cart().Add(Emma, "Emma", 8.75m, quantity: 4);

    Assert.InRange(cart.Total(discountRate: 0.10m), 0m, cart.Subtotal);
}

[Fact]
public void Parcel_weight_is_compared_with_a_tolerance()
{
    Cart cart = new Cart().Add(Hobbit, "The Hobbit", 9.90m, quantity: 3, weightKg: 0.1);

    Assert.Equal(0.3, cart.WeightKg, tolerance: 0.000001);
}

decimal para için doğru tip ve Assert.Equal onu birebir karşılaştırıyor. double ise ikili kesirle çalıştığı için üç tane 0,1'in toplamı 0,3 etmiyor. Tolerance'sız yazılmış aynı test şöyle kalıyor:

Assert.Equal() Failure: Values differ
Expected: 0.29999999999999999
Actual:   0.30000000000000004

Mesaj iki değerin de tam ikili gösterimini yazıyor; böyle bir çıktı gördüğünde sorun hesapta değil karşılaştırmada. double ve float için iki seçenek var: tolerance (fark en fazla şu kadar olabilir) ya da precision (iki değer de bu kadar ondalığa yuvarlanıp karşılaştırılır; MidpointRounding alan overload'u da var). decimal için de precision alan bir overload var, ama para hesabında yuvarlama iş kuralının parçasıysa onu production kodunda yap ve testte tam değeri bekle; yukarıdaki indirim testi böyle.

Tarihler için Assert.Equal(DateTimeOffset, DateTimeOffset, TimeSpan) overload'u farkı bir süreyle sınırlıyor. Saati sabitlemek daha iyi çözüm (FakeTimeProvider, 11. yazı); tolerance'ı yalnız gerçek saat kaçınılmazsa kullan.

Eşitlik ailesinin diğer üyeleri kısa: Assert.Same(cart, cart.Add(...)) iki referansın aynı instance olduğunu kontrol ediyor (fluent method'un yeni sepet değil aynı sepeti döndüğünü). Assert.Null/NotNull arama sonucu için, Assert.InRange sınırı olan ama tek doğru değeri olmayan sonuçlar için (indirimli toplam sıfırla ara toplam arasında). Assert.Equal bir IEqualityComparer<T> ya da Func<T, T, bool> da alabiliyor; elinde domain'e özgü bir eşitlik kuralı varsa onu testte yeniden yazmak yerine comparer olarak ver. Assert.StrictEqual ise yalnız tipin varsayılan comparer'ını (EqualityComparer<T>.Default) kullanıyor; Equal'ın koleksiyonlar için yaptığı element element karşılaştırmaya girmiyor.

String ve Span Assertion'ları

[Fact]
public void Order_number_has_year_and_six_digit_sequence()
{
    string number = OrderNumber.Format(2026, 123);

    Assert.Matches(@"^BS-\d{4}-\d{6}$", number);
    Assert.StartsWith("BS-2026-", number);
}

[Fact]
public void Receipt_lists_lines_and_total()
{
    string expected = """
        Order BS-2026-000123
        9780441172719 x2 25.00
        9780261103344 x1 9.90
        Total 34.90
        """;

    string receipt = Receipt.Render(DuneAndHobbit());

    Assert.Equal(expected, receipt, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}

[Fact]
public void Isbn_prefix_is_checked_without_allocating()
{
    ReadOnlySpan<char> isbn = Dune.AsSpan();

    Assert.StartsWith("978", isbn);
    Assert.Equal("0441172719", isbn[3..]);
}

Fiş metni Environment.NewLine ile üretiliyor ve kolonları iki boşlukla hizalıyor. Beklenen metin raw string literal; satır sonu, dosyanın git'ten hangi satır sonuyla çekildiğine bağlı. ignoreLineEndingDifferences \r\n, \r ve \n'i eşit sayıyor, ignoreWhiteSpaceDifferences boşluk dizilerini tek boşluk gibi karşılaştırıyor; ignoreAllWhiteSpace ve ignoreCase de aynı overload'da. Bu iki ayar olmadan aynı test şu mesajla kalıyor:

Assert.Equal() Failure: Strings differ
                                        ↓ (pos 35)
Expected: ···"026-000123\n9780441172719 x2 25.00\n9780261103344 x1"···
Actual:   ···"026-000123\n9780441172719  x2  25.00\n9780261103344 "···
                                        ↑ (pos 35)

String farkında xUnit ilk farklı karakterin pozisyonunu okla işaretliyor ve uzun metni o noktanın çevresine kırpıyor. Bu mesaj, fişin hizalamasını gerçekten test etmek istiyorsan ayarları kaldırman gerektiğini de söylüyor: ignoreWhiteSpaceDifferences açıkken iki boşluk yerine tek boşluk üreten bir hata yakalanmaz.

Assert.Matches regex ile, StartsWith/EndsWith/Contains alt string ile çalışıyor ve hepsinin StringComparison alan overload'u var. v3'te Span<char>, ReadOnlySpan<char> ve Memory<char> için aynı method'lar string gibi davranıyor; genel Span<T> ve Memory<T> için de Equal, Contains ve DoesNotContain var. ISBN'i alt string'e kopyalamadan kontrol eden son test bu overload'ları kullanıyor.

Koleksiyon Assertion'ları

[Fact]
public void Lines_keep_the_cart_order()
{
    Assert.Collection(order.Lines,
        first => Assert.Equal(new OrderLine(Dune, 2, 12.50m), first),
        second => Assert.Equal(Hobbit, second.Isbn));
}

[Fact]
public void Every_line_has_a_positive_quantity_and_price()
{
    Assert.All(order.Lines, line =>
    {
        Assert.True(line.Quantity > 0);
        Assert.True(line.UnitPrice > 0m);
    });
}

[Fact]
public void Lines_are_searched_by_condition()
{
    Assert.Contains(order.Lines, l => l.Isbn == Dune && l.Quantity == 2);
    Assert.DoesNotContain(order.Lines, l => l.Isbn == Emma);
    Assert.Distinct(order.Lines.Select(l => l.Isbn));
}

[Fact]
public void Ordered_isbns_come_from_the_catalog()
{
    HashSet<string> catalog = [Dune, Hobbit, Emma];

    Assert.Subset(catalog, order.Lines.Select(l => l.Isbn).ToHashSet());
}

[Fact]
public void Adding_the_same_isbn_twice_merges_the_line()
{
    Cart cart = new Cart()
        .Add(Dune, "Dune", 12.50m)
        .Add(Dune, "Dune", 12.50m);

    CartLine line = Assert.Single(cart.Lines);
    Assert.Equal(2, line.Quantity);
}

Assert.Collection sırayı ve sayıyı birlikte doğruluyor: her element için bir inspector veriyorsun. Sayı tutmazsa "Mismatched item count" ile beklenen ve gerçek sayıyı, bir inspector kalırsa hangi pozisyonda kaldığını yazıyor:

Assert.Collection() Failure: Item comparison failure
                                                                                  ↓ (pos 1)
Collection: [OrderLine { Isbn = 9780441172719, Quantity = 2, UnitPrice = 12.50 }, OrderLine { Isbn = 9780261103344, Quantity = 1, UnitPrice = 9.90 }]
Error:      Assert.Equal() Failure: Values differ
            Expected: 2
            Actual:   1

Assert.All her elementi aynı kuralla kontrol ediyor ve kalan elementlerin index'ini listeliyor. Assert.Single hem "tam bir element" kontrolü yapıyor hem de o elementi döndürüyor; Assert.Equal(1, list.Count) yazarsan xUnit analyzer'ı xUnit2013 uyarısıyla seni buraya yönlendiriyor. Benzer şekilde Assert.True(lines.Any(...)) xUnit2012 uyarısı veriyor ve Assert.Contains'i öneriyor, çünkü Contains kalınca koleksiyonun tamamını mesaja yazıyor. Assert.Subset ve Superset set'lerle çalışıyor; yukarıda siparişteki ISBN'lerin katalogda olduğunu kontrol ettik. Assert.Distinct tekrar eden elementi, Assert.Empty ve NotEmpty boşluğu kontrol ediyor.

Bir ayrım önemli: Assert.Equal iki koleksiyonu sıralı karşılaştırıyor ve ilk farklı pozisyonu okla gösteriyor; Assert.Equivalent sırayı yok sayıyor. Siparişte satır sırası anlamlıysa (fişte görünen sıra) Equal ya da Collection, değilse Equivalent kullan. Hangisini seçtiğin testin söylediği şeyin parçası.

Equivalent ile Object Graph Karşılaştırması

[Fact]
public void Lines_match_regardless_of_order()
{
    Assert.Equivalent(
        new[]
        {
            new { Isbn = Hobbit, Quantity = 1, UnitPrice = 9.90m },
            new { Isbn = Dune, Quantity = 2, UnitPrice = 12.50m },
        },
        order.Lines,
        strict: true);
}

[Fact]
public void Order_from_the_same_cart_differs_only_by_id()
{
    Cart cart = new Cart()
        .Add(Dune, "Dune", 12.50m, quantity: 2)
        .Add(Hobbit, "The Hobbit", 9.90m);

    Order created = Order.FromCart(Guid.NewGuid(), "BS-2026-000123", "ada@example.com", cart);

    Assert.EquivalentWithExclusions(order, created, o => o.Id);
}

Assert.Equivalent tip bakmadan public property ve field'ları özyinelemeli karşılaştırıyor; anonim object ile record'u, DTO (Data Transfer Object; katmanlar arasında veri taşıyan sade nesne) ile entity'yi yan yana koyabiliyorsun. Önce Tahmin Et'teki testin düzeltilmiş hâli ilk test: beklenen tarafta artık üç member da var ve strict: true ile yarın OrderLine'a eklenecek yeni bir property de testin dikkatinden kaçmayacak.

Ters yön de hata: beklenen tarafta gerçek object'te olmayan bir member varsa, strict olmasa bile Equivalent kalıyor:

Assert.Equivalent() Failure: Mismatched member list
Expected: ["Isbn", "Quantity", "Discount"]
Actual:   ["Isbn", "Quantity", "UnitPrice"]

EquivalentWithExclusions belirli member'ları karşılaştırmanın dışında bırakıyor. İkinci test aynı sepetten oluşturulan iki siparişin Id dışında eşdeğer olduğunu söylüyor. İfade tabanlı overload'un yanında "Member.SubMember" biçiminde string yol alan overload'u da var, ama bu yol koleksiyon elementlerine inmiyor: EquivalentWithExclusions(a, b, "Lines.UnitPrice") denediğimde fiyatlar yine karşılaştırıldı ve test "Collection value not found in member 'Lines[]'" mesajıyla kaldı. AwesomeAssertions'ın BeEquivalentTo'su gibi zengin seçenekleri yok; ihtiyacın ondan fazlaysa bir sonraki yazıya bak.

Exception ve Event Assertion'ları

[Fact]
public async Task Placing_more_than_available_throws_insufficient_stock()
{
    OrderPlacementService service = new(stock);

    InsufficientStockException ex = await Assert.ThrowsAsync<InsufficientStockException>(
        () => service.PlaceAsync(DuneAndHobbit(), TestContext.Current.CancellationToken));

    Assert.Equal(Dune, ex.Isbn);
    Assert.Equal(2, ex.Requested);
    Assert.Equal(1, ex.Available);
}

[Fact]
public void Zero_quantity_is_rejected_with_the_parameter_name()
{
    Cart cart = new();

    Assert.Throws<ArgumentOutOfRangeException>("quantity", () => cart.Add(Dune, "Dune", 12.50m, quantity: 0));
}

[Fact]
public void Placing_twice_names_the_current_status()
{
    Order order = DuneAndHobbit();
    order.MarkPlaced();

    Assert.Throws<InvalidOperationException>(
        () => order.MarkPlaced(),
        ex => ex.Message.Contains("already Placed") ? null : $"Message does not name the status: {ex.Message}");
}

Throws ve ThrowsAsync tam tip eşleşmesi istiyor: InsufficientStockException, InvalidOperationException'dan türese bile ThrowsAsync<InvalidOperationException> kalıyor ve mesaj iki tipi de yazıyor ("Exception type was not an exact match"). Türeyen tipleri kabul etmek istiyorsan ThrowsAny/ThrowsAnyAsync kullan. Parametre adı alan overload ArgumentException ailesinde ParamName'i de kontrol ediyor. Üçüncü test v3'ün inspector overload'unu kullanıyor: fonksiyon null dönerse geçer, string dönerse o string hata mesajı olur:

Assert.Throws() Failure: Message does not name the status: Order BS-2026-000123 is already Placed.

Asıl tuzak async kodda. Assert.Throws<T>(() => service.PlaceAsync(...)) yazarsan lambda Func<Task> olarak çözülüyor ve derleme iki hatayla duruyor: overload'un obsolete mesajı (CS0619, "You must call Assert.ThrowsAsync<T> (and await the result) when testing async code.") ve analyzer'ın xUnit2014 hatası. Aynı CS0619 hatasını, () => throw new X() biçimindeki bir lambda'da da aldım; derleyici throw ifadesini her dönüş tipine uydurabildiği için Func<Task> overload'u seçildi. Senkron kod için lambda'yı bir method çağrısıyla yaz.

Event'ler için Raises, RaisesAny ve async karşılıkları var:

[Fact]
public void Marking_placed_raises_order_placed_with_the_total()
{
    Order order = DuneAndHobbit();

    Assert.RaisedEvent<OrderPlacedEventArgs> raised = Assert.Raises<OrderPlacedEventArgs>(
        h => order.Placed += h,
        h => order.Placed -= h,
        () => order.MarkPlaced());

    Assert.Same(order, raised.Sender);
    Assert.Equal(new OrderPlacedEventArgs(order.Id, "BS-2026-000123", 34.90m), raised.Arguments);
}

[Fact]
public void Marking_placed_notifies_status_change()
{
    Order order = DuneAndHobbit();

    Assert.PropertyChanged(order, nameof(Order.Status), order.MarkPlaced);
}

[Fact]
public async Task Failed_reservation_raises_no_order_placed()
{
    Order order = DuneAndHobbit();
    OrderPlacementService service = new(new InMemoryStockLedger());

    await Assert.NotRaisedAnyAsync<OrderPlacedEventArgs>(
        h => order.Placed += h,
        h => order.Placed -= h,
        () => Assert.ThrowsAsync<InsufficientStockException>(
            () => service.PlaceAsync(order, TestContext.Current.CancellationToken)));
    Assert.Equal(OrderStatus.Draft, order.Status);
}

Raises handler'ı bağlayıp kodu çalıştırıyor, event'i yakalayıp Sender ve Arguments ile döndürüyor; event hiç gelmezse "No event was raised" diyor. Raises<T> argüman tipinde tam eşleşme istiyor, türeyen tipleri RaisesAny<T> kabul ediyor. PropertyChanged, INotifyPropertyChanged uygulayan bir object'in o property için bildirim yaptığını kontrol ediyor. Son test iki assertion'ı iç içe kullanıyor: stok yetersizken PlaceAsync'in exception fırlattığını ve Placed event'inin hiç yayınlanmadığını tek akışta doğruluyor.

Dinamik Skip ve Kendi Assertion Helper'ın

v3 bir testin çalışırken atlanmasını sağlıyor: Assert.Skip(reason), SkipUnless(condition, reason) ve SkipWhen(condition, reason). Fiyatı Türk lirası biçiminde yazan test, kültür verisi olmayan ortamlarda (globalization-invariant mode ile çalışan küçük container image'ları gibi) anlamsız:

[Fact]
public void Total_is_formatted_in_turkish_lira()
{
    Assert.SkipUnless(CultureDataAvailable("tr-TR"), "tr-TR culture data is not available (globalization-invariant mode).");

    string total = Receipt.FormatTotal(DuneAndHobbit(), "tr-TR");

    Assert.Equal("₺34,90", total);
}

İlk denememde koşulu AppContext.TryGetSwitch("System.Globalization.Invariant", ...) ile yazdım. Testleri DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ile çalıştırınca test atlanmadı, CultureNotFoundException ile kaldı: environment variable ile açılan mod o switch'e yansımıyordu. Doğru koşul, kültürü gerçekten istemek ve CultureNotFoundException'ı yakalamak. Aynı komutla çıktı artık:

skipped Bookstore.Ordering.Tests.ReceiptTests.Total_is_formatted_in_turkish_lira
  tr-TR culture data is not available (globalization-invariant mode).

Skip'i "bu ortamda anlamsız" durumları için kullan; kırık bir testi susturmak için değil. Atlanan test CI raporunda görünür ama yeşil sayılır.

Aynı kontrol birçok testte tekrar ediyorsa kendi assertion helper'ını yaz:

internal static class OrderAssert
{
    public static OrderLine HasLine(Order order, string isbn, int quantity)
    {
        OrderLine? line = order.Lines.FirstOrDefault(l => l.Isbn == isbn);
        if (line is null || line.Quantity != quantity)
        {
            throw new OrderAssertException(
                $"OrderAssert.HasLine() Failure: {order.OrderNumber} has no line {isbn} x{quantity}" + Environment.NewLine +
                $"Lines: {string.Join(", ", order.Lines.Select(l => $"{l.Isbn} x{l.Quantity}"))}");
        }

        return line;
    }
}

public sealed class OrderAssertException(string message) : XunitException(message);

Helper iki şeyi xUnit gibi yapıyor: bulduğu satırı döndürüyor (testte Assert.Single gibi devam edebilesin diye) ve mesajda neyi aradığını ve ne bulduğunu yazıyor. XunitException'dan türeyen exception, IAssertionException marker interface'ini uyguluyor. v3 test sonucuna bir failure cause (hatanın kaynağı: assertion, exception, timeout) ekliyor ve bu interface'i uygulayan exception'ı assertion hatası sayıyor. Fark çıktıda görünüyor:

failed Bookstore.Ordering.Tests.FailDemo.Helper_fails (0ms)
  Bookstore.Ordering.Tests.OrderAssertException : OrderAssert.HasLine() Failure: BS-2026-000123 has no line 9780441172719 x3
  Lines: 9780441172719 x2, 9780261103344 x1
failed Bookstore.Ordering.Tests.FailDemo.Plain_exception_fails (3ms)
  Xunit.MicrosoftTestingPlatform.XunitException: System.InvalidOperationException : Order BS-2026-000123 has no line 9780441172719 x3

Düz bir InvalidOperationException fırlatan helper, testteki beklenmedik bir hatadan ayırt edilemiyor. xUnit'e bağımlılık istemeyen bir helper kütüphanesi yazıyorsan, interface'in adı yeterli: herhangi bir namespace'te IAssertionException adında boş bir interface tanımlayıp onu uygulamak aynı sonucu veriyor.

Üretimde Dikkat

Analyzer uyarılarını hata olarak ele al. xunit.analyzers paketi xunit.v3 ile birlikte geliyor; bu yazıdaki örneklerde xUnit2000 (sabit değer expected tarafında olmalı), xUnit2012 (Assert.True yerine Contains) ve xUnit2013 (Equal(1, Count) yerine Single) uyarılarını gördüm. Hepsi daha okunur bir hata mesajına götürüyor. Analyzer her şeyi yakalamıyor: Assert.True(order.Lines.Count == 2) ve Assert.Equal(1, lines.Count(predicate)) 2.3.0'da uyarı vermedi.

CI'daki ortam senin makinen değil. Kültür verisi, satır sonları, saat dilimi ve dosya yolu ayıracı test ortamına göre değişir. Satır sonunu ignoreLineEndingDifferences ile, kültürü testin içinde açıkça seçerek (CultureInfo.GetCultureInfo("tr-TR")) ve kültür verisi gerektiren testleri SkipUnless ile koruyarak bu farkları testin dışına çıkar.

Hata mesajını oku ve gerekiyorsa uzat. xUnit uzun koleksiyon ve string'leri kırpıyor; Assert.OverrideMaxEnumerableLength, OverrideMaxStringLength gibi method'larla bir test içinde bu sınırları değiştirebilirsin. Gerçekten 500 elementli bir koleksiyonu mesajda görmen gerekiyorsa genelde assertion yanlış seviyede yazılmıştır: önce tek bir elementi ya da sayıyı doğrula.

Sık Düşülen Tuzaklar

Assert.Equal(expected, actual) sırasını ters yazmak. Assert.Equal(order.Total, 35.90m) derlenir ve xUnit2000 uyarısı verir. Uyarıyı görmezden gelirsen mesaj seni yanıltır:

Assert.Equal() Failure: Values differ
Expected: 34.90
Actual:   35.90

Burada 34,90 aslında sistemin ürettiği değer, 35,90 ise testin beklediği. Ters sıralı bir mesajla hata ayıklamak, doğru kodu "düzeltmeye" kadar gidebilir.

double'da tolerance'sız Equal. Ağırlık, oran ya da ortalama gibi double sonuçlarda tolerance ya da precision ver. Para için decimal kullan ve beklenen değeri elle hesapla.

Equivalent'ın fazladan property'yi görmezden gelmesi. Önce Tahmin Et'teki test yeşil kaldı ama UnitPrice'ı hiç kontrol etmiyordu. Beklenen object'i eksik yazıyorsan bunu bilerek yap; bir DTO'nun tamamını doğruluyorsan strict: true kullan.

Tip kontrolünde IsType'ın tam eşleşme istemesi. Assert.IsType<InvalidOperationException>(ex) türeyen InsufficientStockException için "Value is not the exact type" diye kalıyor. Hiyerarşiyi kabul etmek istiyorsan IsAssignableFrom<T> ya da v3'teki IsType<T>(obj, exactMatch: false) overload'unu kullan.

Küçük Bir Deneme

★☆☆ Books.DuneAndHobbit()'taki Dune satırının fiyatını bilerek 0m yap ve Lines_are_two_dune_and_one_hobbit'i çalıştır. Test kalıyor mu? Sonra strict: true ekle ve mesajı oku.

★★☆ OrderAssert.HasLine'a fiyat kontrolü ekle ve helper'ı Assert.Multiple içinde iki kez çağıran bir test yaz. İki satır birden yanlışsa mesajda ikisi de görünüyor mu?

★★★ Receipt.Render için ignoreWhiteSpaceDifferences kullanmadan çalışan bir test yaz ve testleri hem \n hem \r\n satır sonlu bir dosyayla çalıştır (git'te core.autocrlf ayarını değiştirerek). Hangi ayar hangi hatayı gizliyor?

Bir sonraki yazıda aynı kontrolleri AwesomeAssertions ve Shouldly ile yazıyoruz: Should() zinciri, BeEquivalentTo seçenekleri (Excluding, WithStrictOrdering, Using), tarih için BeCloseTo, birden fazla hatayı tek raporda toplayan AssertionScope ve FluentAssertions'tan taşınma.