AwesomeAssertions ve Shouldly Rehberi: Should(), BeEquivalentTo Seçenekleri, Collection, Exception, Tarih ve Assertion Scope
AwesomeAssertions'ta BeEquivalentTo, Equals'ı override eden tipleri member'larına inmeden Equals ile karşılaştırır; Id'ye göre eşitlenen entity'lerde bu, farklı iki siparişi eşit sayar ve çözümü ComparingByMembers<Entity>()'dir. Excluding, For().Exclude, Using/WhenTypeIs, WithStrictOrdering, Throw/ThrowExactly, BeCloseTo ve AssertionScope; Shouldly'nin kaynak koddan mesaj üretmesi ve FluentAssertions 7'den AwesomeAssertions 9'a gerçek taşıma hatalarıyla.
Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3, AwesomeAssertions 9.6.0 ve Shouldly 4.3.0 ile derlenip denenmiştir; taşıma örneğinde FluentAssertions 7.2.0 kullanıldı.
Önce Tahmin Et
03. yazıda assertion kütüphanelerini lisanslarıyla birlikte tanıtmış ve seride yerleşik Assert'te kalmıştık. Ekibin zaten AwesomeAssertions ya da Shouldly kullandığı bir projeye düştüğünde ihtiyacın olan şey API'nin kendisi; bu yazı onu sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) üzerinden aile aile geçiyor. Önce bir soru. Bookstore'un persistence katmanındaki entity'ler ortak bir base class'tan türüyor ve DDD (Domain-Driven Design; iş alanını modelin merkezine koyan tasarım yaklaşımı) kitaplarındaki gibi Id'ye göre eşitleniyor:
public abstract class Entity
{
public Guid Id { get; init; }
public override bool Equals(object? obj) => obj is Entity other && other.GetType() == GetType() && other.Id == Id;
public override int GetHashCode() => Id.GetHashCode();
}
public sealed class OrderRecord : Entity
{
public required string OrderNumber { get; init; }
public required string CustomerEmail { get; init; }
public OrderStatus Status { get; set; }
public List<OrderLineRecord> Lines { get; init; } = [];
public DateTimeOffset CreatedAtUtc { get; init; }
public string CreatedBy { get; init; } = "";
public DateTimeOffset? UpdatedAtUtc { get; set; }
}
Kaydedilen siparişin geri okunduğunda aynı olduğunu doğrulayan bir test, okuma kodundaki bir hatayı taklit ediyor:
[Fact]
public void Reloaded_order_is_equivalent_to_the_saved_one()
{
OrderRecord saved = SavedOrder();
OrderRecord reloaded = SavedOrder();
reloaded.Status = OrderStatus.Draft;
reloaded.Should().BeEquivalentTo(saved);
}
saved Placed, reloaded Draft. BeEquivalentTo property'leri tek tek karşılaştırdığına göre test kalmalı. Kalıyor mu?
Cevap: Equals'ı Override Eden Tip Değer Gibi Karşılaştırılıyor
Test geçiyor. AwesomeAssertions'ın equivalency motoru bir tipin Equals'ı override ettiğini görünce onu value type gibi ele alıyor ve member'lara inmeden Equals'ı çağırıyor. Entity.Equals yalnız Id'ye baktığı için iki sipariş eşit sayılıyor. Member bazında karşılaştırmayı açıkça istemek gerekiyor:
reloaded.Should().BeEquivalentTo(saved, options => options.ComparingByMembers<OrderRecord>());
Expected property reloaded.Status to equal OrderStatus.Placed {value: 1} by value, but found OrderStatus.Draft {value: 0}.
With configuration:
- Prefer the declared type of the members
- Compare enums by value
- Compare tuples by their properties
- Compare anonymous types by their properties
- Compare records by their members
- Compare Bookstore.Ordering.OrderRecord by its members
- Include non-browsable members
- Match member by name (or throw)
- Be strict about the order of items in byte arrays
- Without automatic conversion.
Bu da yarım çözüm. OrderLineRecord da bir Entity ve ComparingByMembers<OrderRecord>() yalnız sipariş tipine uygulanıyor: bir satırın adedini 2'den 5'e çevirdiğimde test yine geçti. ComparingByMembers<Entity>() ise base class'tan türeyen bütün tiplere uygulanıyor ve satırdaki farkı da yakaladı ("Expected property Changed().Lines[0].Quantity to be 2, but found 5."). Her testte bunu yazmamak için ayarı assembly genelinde bir kez yapıyoruz:
[assembly: AssertionEngineInitializer(typeof(Bookstore.Ordering.AwesomeAssertions.Tests.AssertionSetup), nameof(Bookstore.Ordering.AwesomeAssertions.Tests.AssertionSetup.Initialize))]
namespace Bookstore.Ordering.AwesomeAssertions.Tests;
public static class AssertionSetup
{
// Entity'ler Equals'ı Id'ye göre override ediyor; her BeEquivalentTo'da member bazında karşılaştır
public static void Initialize() =>
AssertionConfiguration.Current.Equivalency.Modify(options => options.ComparingByMembers<Entity>());
}
AssertionEngineInitializer, ilk assertion çalışmadan önce verilen static method'u çağırıyor. Bu ayarla Önce Tahmin Et'teki test, hiçbir değişiklik yapmadan doğru şekilde kalıyor. Records için tersi geçerli: 6.0'dan beri record'lar varsayılan olarak member bazında karşılaştırılıyor (yukarıdaki listede "Compare records by their members").
Should() Zinciri: Temel Assertion'lar
AwesomeAssertions'ta her assertion Should() ile başlıyor ve dönen tipin method'larıyla devam ediyor. And aynı subject üzerinde zinciri sürdürüyor, Which ise bir önceki assertion'ın bulduğu elemente geçiyor:
[Fact]
public void Dto_total_and_status()
{
OrderDto dto = OrderMapper.ToDto(saved);
dto.Total.Should().Be(43.65m);
dto.Status.Should().Be(OrderStatus.Placed);
dto.CustomerEmail.Should().EndWith("@example.com").And.NotContainAny(" ", "\t");
}
Sayılar için Be, BePositive, BeGreaterThan, BeInRange ve double/decimal için BeApproximately(expected, precision) var. String'lerde StartWith, Contain, Match (wildcard) ve MatchRegex; büyük-küçük harf duyarsız karşılaştırma için BeEquivalentTo (string'de "aynı metin, harf farkı önemsiz" demek). Mesajlar kaynak koddaki ifadeyi kullanıyor; order.Total.Should().Be(37.40m) kalınca şunu yazıyor:
Expected order.Total to be 37.40M, but found 34.90M (difference of -2.50).
Fark değerinin de yazılması küçük ama faydalı: 2,50 EUR'luk bir fark, bir satırın fiyatıyla ilgili bir hatayı hemen düşündürüyor.
BeEquivalentTo Seçenekleri
Asıl güç burada. Persistence entity'sinden üretilen OrderDto'nun, Id ve audit alanları dışında entity'deki her şeyi taşıdığını doğrulayalım. DTO (Data Transfer Object; katmanlar arasında veri taşıyan sade nesne) fiyatları iki haneye yuvarlıyor ve satırları başlığa göre sıralıyor; entity'de fiyatlar fiyat listesinden dört haneli geliyor:
[Fact]
public void Dto_carries_everything_but_ids_and_audit_fields()
{
OrderDto dto = OrderMapper.ToDto(saved);
dto.Should().BeEquivalentTo(saved, options => options
.Excluding(o => o.Id)
.Excluding(o => o.CreatedAtUtc)
.Excluding(o => o.CreatedBy)
.Excluding(o => o.UpdatedAtUtc)
.For(o => o.Lines).Exclude(l => l.Id)
.Using<decimal>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.005m))
.WhenTypeIs<decimal>());
}
Karşılaştırmayı expectation (beklenen object, burada entity) yönetiyor: entity'deki her member'ın DTO'da karşılığı aranıyor, DTO'daki fazladan Total dikkate alınmıyor. Hiç seçenek vermeden çalıştırınca mesaj neyin eksik olduğunu tek tek sayıyor:
Expected property OrderMapper.ToDto(saved).Lines[0].UnitPrice to be 12.4950M, but found 12.50M.
Expectation has property Lines[0].Id that the other object does not have.
Expectation has property Lines[1].Id that the other object does not have.
Expected property OrderMapper.ToDto(saved).Lines[2].UnitPrice to be 8.7499M, but found 8.75M.
Expectation has property Lines[2].Id that the other object does not have.
Expectation has property CreatedAtUtc that the other object does not have.
Expectation has property CreatedBy that the other object does not have.
Expectation has property UpdatedAtUtc that the other object does not have.
Expectation has property Id that the other object does not have.
Bu listeden seçenekler doğrudan çıkıyor. Excluding tek bir member'ı, For(o => o.Lines).Exclude(l => l.Id) koleksiyondaki her elementin bir member'ını çıkarıyor. Tip bazında kural için Using<T>(...) ile karşılaştırmanın kendisini veriyorsun ve WhenTypeIs<T>() ile hangi member'lara uygulanacağını seçiyorsun; burada bütün decimal'ler yarım kuruş toleransla karşılaştırılıyor. Diğerleri: Including (yalnız seçtiğin member'lar), ExcludingMembersNamed("Id") (her seviyede bu isimdeki member'lar), ExcludingMissingMembers() (expectation'da olup subject'te olmayanları sessizce atla) ve WithMapping (farklı isimli member'ları eşle). ExcludingMissingMembers cazip görünür ama yukarıdaki listeyi susturur; neyi hariç tuttuğunu tek tek yazmak testin ne doğruladığını da belgeler.
Sıra varsayılan olarak önemsiz. DTO'da satırların başlığa göre listelenmesi bir gereksinim olduğu için o testte sırayı açıkça istiyoruz:
[Fact]
public void Dto_lines_are_listed_by_title()
{
OrderDto dto = OrderMapper.ToDto(saved);
dto.Lines.Should().BeEquivalentTo(
[
new { Title = "Dune", Quantity = 2, UnitPrice = 12.50m },
new { Title = "Emma", Quantity = 1, UnitPrice = 8.75m },
new { Title = "The Hobbit", Quantity = 1, UnitPrice = 9.90m },
],
options => options.WithStrictOrdering());
}
Sıra bozulduğunda mesaj hangi index'te hangi string'in beklendiğini, ilk farklı karakteri okla göstererek yazıyor:
Expected property OrderMapper.ToDto(saved).Lines[1].Title to be the same string, but they differ at index 0:
↓ (actual)
"Emma"
"The Hobbit"
↑ (expected).
WithStrictOrderingFor(o => o.Lines) yalnız bir koleksiyon için sıra istiyor; WithoutStrictOrdering() global bir ayarı tek testte geri alıyor.
Koleksiyon, Exception ve Tarih
[Fact]
public void Lines_collection_assertions()
{
dto.Lines.Should().HaveCount(3)
.And.OnlyHaveUniqueItems(l => l.Isbn)
.And.BeInAscendingOrder(l => l.Title, StringComparer.Ordinal)
.And.AllSatisfy(l => l.Quantity.Should().BePositive());
dto.Lines.Should().ContainSingle(l => l.Isbn == Dune)
.Which.Quantity.Should().Be(2);
dto.Lines.Should().ContainEquivalentOf(new { Isbn = Emma, UnitPrice = 8.75m })
.And.NotContain(l => l.UnitPrice <= 0m);
}
ContainSingle xUnit'teki Assert.Single gibi hem kontrol ediyor hem de Which ile elementi veriyor; kalınca kaç element bulduğunu söylüyor ("to contain a single item matching (l.Quantity == 1), but 2 such items were found."). ContainEquivalentOf koleksiyonda equivalency kurallarıyla eşleşen bir element arıyor, AllSatisfy her elemente bir assertion uyguluyor.
Exception'lar için önce bir Action ya da Func<Task> tanımlıyorsun:
[Fact]
public void Placing_twice_is_rejected()
{
Order order = NewOrder();
order.MarkPlaced();
Action act = () => order.MarkPlaced();
act.Should().ThrowExactly<InvalidOperationException>()
.WithMessage("Order BS-2026-000123 is already *");
}
[Fact]
public async Task Insufficient_stock_names_the_book()
{
OrderPlacementService service = new(new InMemoryStockLedger().Receive(Dune, 1));
Func<Task> act = () => service.PlaceAsync(NewOrder(), TestContext.Current.CancellationToken);
await act.Should().ThrowAsync<InsufficientStockException>()
.Where(e => e.Isbn == Dune && e.Requested == 2 && e.Available == 1);
}
Burada xUnit'ten bir fark var: Throw<T> türeyen tipleri de kabul ediyor, tam eşleşme için ThrowExactly<T> gerekiyor. WithMessage wildcard (*, ?) kullanıyor; mesajın tamamını sabitlemek yerine anlamlı kısmını kontrol etmek testin metin değişikliklerine dayanmasını sağlıyor. Where exception'ın property'lerine koşul koyuyor, WithParameterName ArgumentException ailesinde parametre adına bakıyor, WithInnerException<T> iç exception'a iniyor. Async kodda ThrowAsync'i await etmeyi unutma; unutursan assertion hiç sonuçlanmadan test geçer (aşağıdaki tuzaklara bak).
Tarihlerde BeCloseTo tolerance'la karşılaştırıyor. AwesomeAssertions.Extensions namespace'i 2.Seconds(), 7.Days() gibi okunur süreler veriyor:
[Fact]
public void Campaign_ends_one_week_after_it_starts()
{
Campaign campaign = Campaign.StartNow("AUTUMN26", 0.15m, TimeSpan.FromDays(7), TimeProvider.System);
campaign.EndsAtUtc.Should().BeCloseTo(DateTimeOffset.UtcNow.AddDays(7), 2.Seconds());
campaign.EndsAtUtc.Should().BeExactly(campaign.StartsAtUtc + 7.Days());
}
Kampanya bitişini 6 gün sonrasıyla karşılaştıran yanlış bir beklentiyle mesaj farkı milisaniyesine kadar yazıyor: "Expected c.EndsAtUtc to be within 2s from <2026-11-20 22:22:07.219512 +0h>, but <2026-11-21 22:22:07.219511 +0h> was off by 23h, 59m, 59s, 999ms and 999.0µs." Gerçek saate bağlı bu test çalışıyor ama ideal değil; TimeProvider'ı FakeTimeProvider ile sabitleyip BeExactly kullanmak (11. yazı) tolerance'a hiç ihtiyaç bırakmıyor. İkinci satır bunun küçük bir örneği: başlangıçla bitiş arasındaki ilişki saatten bağımsız, kesin karşılaştırılabiliyor.
AssertionScope: Bütün Hataları Tek Raporda Toplamak
Ardışık assertion'lar ilk hatada durur. AssertionScope içindeki assertion'lar ise hatalarını biriktiriyor ve scope kapanınca hepsini birlikte raporluyor:
using (new AssertionScope())
{
order.Status.Should().Be(OrderStatus.Placed);
order.Total.Should().Be(34.90m);
order.Lines.Should().HaveCount(2);
}
Sipariş hiç yerleştirilmemiş ve toplam yanlış beklenmişken çıktı:
Expected order.Status to be OrderStatus.Placed {value: 1}, but found OrderStatus.Draft {value: 0}.
Expected order.Total to be 37.40M, but found 34.90M (difference of -2.50).
Scope olmadan aynı test yalnız ilk satırı rapor ediyor. Bu, xUnit'teki Assert.Multiple'ın karşılığı; 03. yazıdaki kural burada da geçerli: tek davranışın birkaç yönünü birlikte kontrol etmek için kullan, ilgisiz davranışları aynı teste toplamak için değil.
Shouldly: ShouldBe ve Kaynak Koddan Gelen Mesaj
Shouldly'de zincir yok; her assertion subject üzerinde doğrudan bir extension method:
[Fact]
public void Order_total_and_lines()
{
Order order = NewOrder();
order.Total.ShouldBe(34.90m);
order.Lines.Count.ShouldBe(2);
order.Lines.ShouldContain(l => l.Isbn == Dune && l.Quantity == 2);
order.Lines.ShouldAllBe(l => l.UnitPrice > 0m);
order.CustomerEmail.ShouldEndWith("@example.com");
}
[Fact]
public void Order_lines_match_regardless_of_order()
{
Order order = NewOrder();
order.Lines.ShouldBe(
[new OrderLine(Hobbit, 1, 9.90m), new OrderLine(Dune, 2, 12.50m)],
ignoreOrder: true);
}
[Fact]
public void Placed_order_satisfies_all_conditions()
{
Order order = NewOrder();
order.MarkPlaced();
order.ShouldSatisfyAllConditions(
() => order.Status.ShouldBe(OrderStatus.Placed),
() => order.Total.ShouldBe(34.90m),
() => order.Lines.ShouldNotBeEmpty());
}
[Fact]
public async Task Insufficient_stock_names_the_book()
{
OrderPlacementService service = new(new InMemoryStockLedger().Receive(Dune, 1));
InsufficientStockException ex = await Should.ThrowAsync<InsufficientStockException>(
() => service.PlaceAsync(NewOrder(), TestContext.Current.CancellationToken));
ex.Available.ShouldBe(1);
}
[Fact]
public void Campaign_ends_one_week_after_it_starts()
{
Campaign campaign = Campaign.StartNow("AUTUMN26", 0.15m, TimeSpan.FromDays(7), TimeProvider.System);
campaign.EndsAtUtc.ShouldBe(DateTimeOffset.UtcNow.AddDays(7), tolerance: TimeSpan.FromSeconds(2));
}
ShouldSatisfyAllConditions, AssertionScope'un karşılığı ve hataları numaralayarak listeliyor:
Shouldly.ShouldAssertException : order
should satisfy all the conditions specified, but does not.
The following errors were found ...
--------------- Error 1 ---------------
order.Status
should be
OrderStatus.Placed
but was
OrderStatus.Draft
--------------- Error 2 ---------------
order.Total
should be
37.40m
but was
34.90m
order.Status ve order.Total ifadeleri mesaja nereden geliyor? Shouldly 4.x, assertion'ı çağıran satırı stack trace'ten bulup kaynak dosyadan okuyor. Bunun bir yan etkisini denerken gördüm: tek satırlık bir test method'u ([Fact] public void Total_one_liner() => NewOrder().Total.ShouldBe(37.40m);) kalınca mesaj "[Fact] public void Total_one_linerNewOrder().Total should be 37.40m but was 34.90m" oldu; satırın başı da ifadeye karıştı. Kaynak dosyaya ulaşamadığında ifade tamamen kayboluyor: testi derleyip kaynak dosyayı yerinden taşıdım ve --no-build ile çalıştırdım; mesaj "25.00m should be 37.40m but was not" oldu, yani gerçek değer subject'in yerine geçti. Testleri derlendikleri makinede çalıştırmayan bir CI düzeninde (build artifact'ini başka bir agent'ta koşmak gibi) mesajları buna göre oku. Shouldly 5.0'ın preview sürümlerinde release notes'a göre bu mekanizma [CallerArgumentExpression] ile değiştirildi ve ShouldSatisfyAllConditions yerini ShouldSatisfy'a bırakıyor (eski ad obsolete alias olarak kalıyor).
İki davranış farkını bil. Should.Throw<T> ve ThrowAsync<T> türeyen tipleri kabul ediyor: Should.Throw<ArgumentException>, ArgumentOutOfRangeException fırlatan bir çağrıda geçti. ShouldBeEquivalentTo 4.3.0'da tip de karşılaştırıyor; bir DTO'yu anonim object'le karşılaştırmak, beklenen tipin anonim tip, gerçek tipin Bookstore.Ordering.OrderDto olduğunu söyleyen bir mesajla kalıyor. Aynı tipten iki object'te ise farkın yolunu (dto → Lines → Element [0] → UnitPrice) ve değerleri gösteriyor. AwesomeAssertions'taki Excluding/Using gibi seçenekler 4.x'te yok; 5.0 preview.2'nin notları ShouldBeEquivalentTo'nun yeniden yazıldığını ve EquivalencyOptions eklendiğini söylüyor, ama preview'ı üretim test projesine almadan önce davranış değişikliklerini (bazı 4.x hatalarının artık geçmesi, string assertion'larının varsayılan olarak büyük-küçük harfe duyarlı olması) oku.
FluentAssertions'tan Taşınmak
Lisans tarafını 03. yazıda anlattık; yazım günü NuGet'te durum aynı: FluentAssertions 7.2.0'ın lisans alanı Apache-2.0, 8.0.0'dan 8.11.0'a kadar ise paketle gelen Xceed lisans dosyası. AwesomeAssertions'ın release notes'u 8.0.0'ı FluentAssertions'ın Apache-2.0 lisanslı, birebir yerine geçebilecek bir fork'u olarak tanımlıyor; 9.0 ile namespace ve assembly adı AwesomeAssertions oldu ve notlara göre 9.0'da işlevsel bir değişiklik yok. Shouldly'nin lisansı ise BSD (kısıtlaması az, ticari kullanıma açık bir açık kaynak lisans ailesi) ailesinden BSD-3-Clause.
FluentAssertions 7.2.0 ile yazılmış eski bir Bookstore test projesinde yalnız paketi değiştirdim:
<PackageReference Include="AwesomeAssertions" Version="9.6.0" />
CampaignTests.cs(1,7): error CS0246: The type or namespace name 'FluentAssertions' could not be found (are you missing a using directive or an assembly reference?)
CampaignTests.cs(12,20): error CS0246: The type or namespace name 'EquivalencyAssertionOptions<>' could not be found (are you missing a using directive or an assembly reference?)
using FluentAssertions satırlarını using AwesomeAssertions ile değiştirince yalnız ikinci tür hata kaldı. Bunlar namespace'ten değil, FluentAssertions 8.0'daki rename'lerden geliyor: EquivalencyAssertionOptions<T> → EquivalencyOptions<T>, ardından ExcludingNestedObjects() → WithoutRecursing() (bu ikincisi CS1061 hatasıyla çıktı). Üç düzeltmeden sonra test yeşil. Yani 8.x'ten geliyorsan iş büyük ölçüde bul-değiştir; 7.x'ten geliyorsan araya 8.0'ın breaking change listesi giriyor (Execute.Assertion yerine AssertionChain, RespectingRuntimeTypes → PreferringRuntimeMemberTypes, HttpResponseMessage assertion'larının kaldırılması gibi). Büyük bir solution'da AwesomeAssertions dokümanı ara adım öneriyor: eski namespace'leri önce csproj'da global Using olarak tanımla, dosyalardaki using'leri sil, sonra paketi ve global using'leri birlikte değiştir.
Shouldly'ye geçiş ise bul-değiştirle yapılamıyor: zincir yapısı, equivalency seçenekleri ve exception API'si farklı. Elinde binlerce FluentAssertions testi varsa AwesomeAssertions'a geçiş birkaç saatlik, Shouldly'ye geçiş birkaç haftalık iş; kararı buna göre ver.
Üretimde Dikkat
Global equivalency ayarını tek yerde tut. AssertionEngineInitializer ile yapılan ComparingByMembers<Entity>() gibi ayarlar o assembly'deki bütün testleri etkiliyor; ayarı test class'larının static constructor'larına dağıtmak, testlerin çalışma sırasına bağlı sonuçlar üretir.
Hata mesajının sonundaki "With configuration" listesi kalabalık görünse de kullanılan ayarların gerçek hâlidir. "Bu test neden geçiyor?" sorusunun cevabı çoğu zaman oradadır; ama global ayarların (burada ComparingByMembers<Entity>) bu listede görünmediğini fark ettim, o yüzden listeye güvenmeden önce initializer'a da bak.
Paket sürümünü sabitle ve sürüm notunu oku. FluentAssertions'ın 7'den 8'e geçişi bir lisans değişikliğiydi; Shouldly 5.0'ın notlarında da equivalency ve string davranışı değişiyor. Central package management (Directory.Packages.props) ile test paketlerinin sürümü tek dosyada durur ve güncelleme bir pull request'te görünür.
Sık Düşülen Tuzaklar
BeEquivalentTo'yu her şeye kullanmak. Bir testin tek amacı toplamı doğrulamaksa dto.Total.Should().Be(43.65m) yeterli; bütün DTO'yu equivalency ile karşılaştırmak, test kalınca hangi davranışın bozulduğunu söylemeyi zorlaştırır ve testin neyi doğruladığını okuyana belirsiz bırakır. Equivalency'yi mapping gibi "her alan taşınmalı" diyen testlere sakla.
Sürüm geçişinde varsayılan karşılaştırma davranışının değişmesi. Record'ların member bazında karşılaştırılması 6.0'da varsayılan oldu; Shouldly 5.0 preview'da ShouldBeEquivalentTo artık Equals override'larını görmezden geliyor. Paket güncellemesinden sonra yeşil kalan bir test suite'i, davranışın aynı kaldığı anlamına gelmez; sürüm notlarının "breaking" bölümünü oku.
Equals'ı override eden tipler. Önce Tahmin Et'teki tuzak entity'lerle sınırlı değil: yalnız birkaç alana bakan Equals yazılmış her value object'te BeEquivalentTo o Equals'a güvenir.
Async exception assertion'ını await etmemek. act.Should().ThrowAsync<T>() bir Task döner; await edilmezse test sonucu beklemeden biter. Stok yeterliyken (yani exception hiç fırlatılmazken) await'siz yazdığım iki test de yeşil geçti. Async test method'unda derleyici CS4014 uyarısı verdi, senkron (void) test method'unda ise hiçbir uyarı çıkmadı. TreatWarningsAsErrors ilkini yakalar, ikincisini yalnız code review.
Küçük Bir Deneme
★☆☆ AssertionSetup'taki global ayarı yorum satırı yap ve Önce Tahmin Et'teki testi çalıştır. Sonra ayarı geri al ve aynı testi ComparingByMembers<OrderRecord>() ile, satır adedini değiştirerek dene.
★★☆ Dto_carries_everything_but_ids_and_audit_fields'ı Excluding zinciri yerine ExcludingMembersNamed ile yeniden yaz. Hangi yazım testin niyetini daha açık söylüyor?
★★★ Shouldly'deki Dto_is_equivalent_to_the_expected_shape testini, beklenen tarafı anonim object olacak şekilde değiştir ve 4.3.0 ile 5.0 preview'ı ayrı bir projede karşılaştır. Hangi sürümde geçiyor?
Bir sonraki yazıda mocking tarafına geçiyoruz: NSubstitute'un Returns, argument matcher'lar, When..Do, Received, Raise, ForPartsOf ve analyzer'ı; Payments ve Notifications module'leri üzerinden.