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

Moq Rehberi: Setup, SetupSequence, It Matcher'ları, Callback, Verify, Strict Mock, Protected ve Mock.Of — FakeItEasy Karşılıklarıyla

Mert Özen 15 Kas 2026 12 dk 3 görüntülenme
Moq Rehberi: Setup, SetupSequence, It Matcher'ları, Callback, Verify, Strict Mock, Protected ve Mock.Of — FakeItEasy Karşılıklarıyla

Moq'ta MockBehavior.Strict ve VerifyAll, servisin collaborator'larıyla her konuşmasını sözleşmeye çevirir: davranışı değiştirmeyen tek bir log satırı bile legacy testi MockException ile kırar. Setup/ReturnsAsync, SetupSequence, It matcher'ları, out parametreleri, Callback, Verify ve VerifyNoOtherCalls, SetupProperty, As<T>(), Protected() ve Mock.Of; sürüm sabitleme, NSubstitute/FakeItEasy eşleme tablosu ve gerçek hata mesajlarıyla.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3, Moq 4.21.0 (Castle.Core 5.2.1) ve FakeItEasy 9.0.1 ile derlenip denenmiştir.

Önce Tahmin Et

Seride NSubstitute kullanıyoruz, ama .NET projelerinin büyük kısmında testler Moq ile yazılmış ve o kodu okuyup bakımını yapmak gerekiyor. Bu yazı Moq'un API'sini sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) eski bir Ordering test projesi üzerinden geçiyor. Konu Kurumsal Sipariş: okul, kütüphane ve şirketlerin en az 20 adetlik, %10 indirimli toplu siparişi. Projede yıllar önce yazılmış bir test var:

public sealed class InstitutionalCheckoutLegacyTests
{
    private readonly MockRepository mocks = new(MockBehavior.Strict);

    [Fact]
    public async Task PlaceAsync_ValidInstitutionalOrder_SavesOrder()
    {
        Mock<IOrderRepository> orders = mocks.Create<IOrderRepository>();
        Mock<IStockService> stock = mocks.Create<IStockService>();
        Mock<IPriceRules> prices = mocks.Create<IPriceRules>();
        Mock<ICheckoutSession> session = mocks.Create<ICheckoutSession>();
        Mock<TimeProvider> clock = mocks.Create<TimeProvider>();
        Mock<ILogger<InstitutionalCheckout>> logger = mocks.Create<ILogger<InstitutionalCheckout>>();
        int available = 100;
        stock.Setup(s => s.TryReserve("9780441172719", 25, out available)).Returns(true);
        prices.Setup(p => p.DiscountRate(It.IsAny<CustomerAccount>(), 25)).Returns(0.10m);
        clock.Setup(c => c.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
        orders.Setup(o => o.NextSequence(2026)).Returns(42);
        orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
        session.SetupSet(s => s.LastOrderNumber = "BS-2026-000042");
        InstitutionalCheckout sut = new(orders.Object, stock.Object, prices.Object, session.Object, clock.Object, logger.Object);

        CheckoutReceipt receipt = await sut.PlaceAsync(
            new CustomerAccount("library@example.org", true, 3),
            [new OrderLine("9780441172719", 25, 12.50m)],
            TestContext.Current.CancellationToken);

        Assert.Equal("BS-2026-000042", receipt.OrderNumber);
        mocks.VerifyAll();
    }
}

Destek ekibi kurumsal siparişlerin log'da görünmesini istedi ve InstitutionalCheckout.PlaceAsync'in sonuna tek satır eklendi:

logger.LogInformation("Placed institutional order {OrderNumber} with {Quantity} copies", number, quantity);

Davranış değişmedi: aynı sipariş, aynı numara, aynı fiyat. Test geçer mi?

Cevap: Strict Mock Her Konuşmayı Sözleşmeye Çeviriyor

Kalıyor:

Moq.MockException : ILogger.Log<FormattedLogValues>(LogLevel.Information, 0, Placed institutional order BS-2026-000042 with 25 copies, null, Func<FormattedLogValues, Exception, string>) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.

MockRepository(MockBehavior.Strict) ile oluşturulan her mock, kurulmamış bir çağrıda exception fırlatıyor. LogInformation bir extension method; içeride ILogger.Log<TState>'i çağırıyor ve o çağrının setup'ı yok. Test, servisin collaborator'larıyla yaptığı her konuşmayı sözleşmeye çevirmiş: bir log satırı, bir property okuma, bir sorgu daha eklemek davranış aynı kalsa da testi kırıyor. Sonundaki mocks.VerifyAll() da ters yönden aynı işi yapıyor: kurulan her setup'ın en az bir kez çağrılmasını şart koşuyor, yani bir sorguyu kaldırmak da testi kırıyor. 08. yazıda bunu refactoring'e dayanıksızlık olarak konuşmuştuk; burada Moq'un hangi API'lerinin buna yol açtığına ve aynı testin nasıl sadeleşeceğine bakıyoruz.

Sadeleştirilmiş hâlin kuralları: mock'lar loose (Moq'un varsayılanı), logger gerçek bir NullLogger<T> (log'u doğrulamak gerekiyorsa 11. yazıdaki FakeLogger), sorgular (stok, fiyat kuralı, saat, sıra numarası) yalnız kurulur ve doğrulanmaz, doğrulama yalnız sistemin dışına çıkan etkiler (siparişin kaydı) ve sonuç üzerinde. Aşağıdaki testlerin hepsi o log satırıyla birlikte yeşil.

Setup, Returns ve SetupSequence

public sealed class InstitutionalCheckoutTests
{
    private const string Dune = "9780441172719";
    private const string Hobbit = "9780261103344";
    private static readonly CustomerAccount School = new("library@example.org", IsInstitutional: true, CompletedOrders: 3);

    private readonly Mock<IOrderRepository> orders = new();
    private readonly Mock<IStockService> stock = new();
    private readonly Mock<ICheckoutSession> session = new();
    private readonly Mock<TimeProvider> clock = new();
    private readonly IPriceRules prices = Mock.Of<IPriceRules>(
        p => p.DiscountRate(It.Is<CustomerAccount>(c => c.IsInstitutional), It.IsAny<int>()) == 0.10m);

    public InstitutionalCheckoutTests()
    {
        int available = 500;
        stock.Setup(s => s.TryReserve(It.IsRegex(@"^97[89]\d{10}$"), It.IsInRange(1, 500, Moq.Range.Inclusive), out available))
            .Returns(true);
        clock.Setup(c => c.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
        orders.Setup(o => o.NextSequence(2026)).Returns(42);
        session.SetupProperty(s => s.LastOrderNumber);
    }

    private InstitutionalCheckout CreateSut() => new(
        orders.Object, stock.Object, prices, session.Object, clock.Object, NullLogger<InstitutionalCheckout>.Instance);

    private Task<CheckoutReceipt> Place(params OrderLine[] lines) =>
        CreateSut().PlaceAsync(School, lines, TestContext.Current.CancellationToken);

    [Fact]
    public async Task Order_numbers_follow_the_repository_sequence()
    {
        orders.SetupSequence(o => o.NextSequence(2026))
            .Returns(42)
            .Returns(43);

        CheckoutReceipt first = await Place(new OrderLine(Dune, 20, 12.50m));
        CheckoutReceipt second = await Place(new OrderLine(Hobbit, 30, 9.90m));

        Assert.Equal("BS-2026-000042", first.OrderNumber);
        Assert.Equal("BS-2026-000043", second.OrderNumber);
    }

    [Fact]
    public async Task Save_timeout_propagates_and_session_is_not_updated()
    {
        orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
            .ThrowsAsync(new TimeoutException("Database did not respond in 30s."));

        await Assert.ThrowsAsync<TimeoutException>(() => Place(new OrderLine(Dune, 20, 12.50m)));

        Assert.Null(session.Object.LastOrderNumber);
    }
}

Moq'ta her şey bir Mock<T> üzerinden kuruluyor ve test edilen koda .Object veriliyor. Setup(x => x.Method(args)) bir lambda expression alıyor; Moq bu expression'ı çözümleyip hangi çağrıyı kurduğunu anlıyor. Dönüş Returns(value), Returns(() => ...) ya da argümanları alan Returns((int year) => ...) ile veriliyor; async method'lar için ReturnsAsync, exception için Throws/ThrowsAsync. Kurulmamış bir Task dönen method loose mock'ta tamamlanmış bir Task dönüyor: SaveAsync'i yalnız exception testinde kurmamızın sebebi bu.

SetupSequence ardışık çağrılara farklı yanıt veriyor; Returns, Throws, ReturnsAsync, ThrowsAsync karışık zincirlenebiliyor (07. yazıdaki zaman aşımı senaryosu böyleydi). Sıra bittiğinde Moq default(T) dönüyor, son değeri tekrarlamıyor; NSubstitute'tan farkı bu.

İki ayrıntı daha. TimeProvider bir interface değil, abstract bir class; GetUtcNow virtual olduğu için Moq onu da taklit edebiliyor. Yine de testte saat için önerimiz 11. yazıdaki FakeTimeProvider: zamanı ilerletebiliyor ve timer'ları da yönetiyor. Mock.Of<T>(predicate), Moq'un LINQ (Language Integrated Query; C#'a gömülü sorgu syntax'ı) to Mocks dediği yazım; kurulumu bir LINQ sorgusu gibi yazdırıyor: "şu çağrı şunu dönen bir IPriceRules ver" diyor ve doğrudan T döndürüyor. Sorgu tipindeki bağımlılıklar için kısa ve okunur; doğrulama gerekirse Mock.Get(prices) ile Mock<T>'ye geri dönebiliyorsun.

It Matcher'ları, out Parametreleri ve Callback

Constructor'daki stok kurulumu üç matcher'ı birlikte kullanıyor: It.IsRegex 13 haneli geçerli bir ISBN (International Standard Book Number; kitabın uluslararası kimlik numarası) biçimini, It.IsInRange(1, 500, Moq.Range.Inclusive) makul bir adet aralığını kabul ediyor. Diğerleri: It.IsAny<T>(), It.Is<T>(predicate), It.IsIn(values)/It.IsNotIn(values), It.IsNotNull<T>(). Mock.Of içindeki It.Is<CustomerAccount>(c => c.IsInstitutional) indirimi yalnız kurumsal müşteri için tanımlıyor; bireysel bir müşteri gelirse kurulum eşleşmiyor ve loose mock 0m dönüyor.

out parametreler Moq'ta kurulum anındaki değişkenin değeriyle dolduruluyor:

[Fact]
public async Task Short_stock_is_reported_with_the_available_count()
{
    int available = 7;
    stock.Setup(s => s.TryReserve(Dune, 25, out available)).Returns(false);

    InsufficientStockException ex = await Assert.ThrowsAsync<InsufficientStockException>(
        () => Place(new OrderLine(Dune, 25, 12.50m)));

    Assert.Equal(7, ex.Available);
    orders.Verify(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never());
}

Değer Setup çağrıldığı anda okunuyor; sonradan available'ı değiştirmek kurulumu etkilemiyor. out parametre eşleştirmeye katılmıyor, yani bu kurulum "Dune, 25 adet" çağrısına uyuyor. Argümana göre farklı out değeri gerekiyorsa her durum için ayrı bir Setup yaz. ref parametreleri eşleştirmek için It.Ref<T>.IsAny var.

Argümanı yakalamak için Callback:

[Fact]
public async Task Bulk_order_is_saved_with_the_institutional_discount()
{
    Order? saved = null;
    orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
        .Callback<Order, CancellationToken>((order, _) => saved = order)
        .Returns(Task.CompletedTask);

    CheckoutReceipt receipt = await Place(new OrderLine(Dune, 20, 12.50m), new OrderLine(Hobbit, 5, 9.90m));

    Assert.Equal(new CheckoutReceipt("BS-2026-000042", 269.55m, clock.Object.GetUtcNow()), receipt);
    Assert.NotNull(saved);
    Assert.Equal([new OrderLine(Dune, 20, 11.25m), new OrderLine(Hobbit, 5, 8.91m)], saved.Lines);
    orders.Verify(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once());
}

Callback'in generic tip parametreleri method imzasıyla aynı olmalı; uyuşmazsa hata derlemede değil, kurulum satırı çalışırken geliyor (Callback<Order> yazınca: "Invalid callback. Setup on method with parameters (Order, CancellationToken) cannot invoke callback with parameters (Order)."). Yakalanan siparişi ayrı assertion'larla kontrol etmek, bütün kontrolü It.Is<Order>(...) içine gömmekten daha okunur bir hata mesajı veriyor; bir sonraki bölümde nedenini göreceğiz.

Verify, Times ve VerifyNoOtherCalls

Verify(expression, Times.X()) bir çağrının kaç kez yapıldığını doğruluyor: Times.Once(), Never(), Exactly(n), AtLeastOnce(), AtMost(n), Between(a, b, Range.Inclusive). Siparişin toplamını Verify içinde kontrol edip yanlış değer beklersek:

Moq.MockException :
Expected invocation on the mock once, but was 0 times: o => o.SaveAsync(It.Is<Order>(x => x.Total == 250), It.IsAny<CancellationToken>())

Performed invocations:

   Mock<IOrderRepository:8> (o):

      IOrderRepository.NextSequence(2026)
      IOrderRepository.SaveAsync(Order, CancellationToken)

Mesaj, yapılan çağrıları listeliyor ama Order bir record olmadığı için yalnız tipin adını yazıyor; toplamın ne olduğunu söylemiyor. Callback ile yakalayıp Assert.Equal ile karşılaştırmayı bu yüzden tercih ettik. 16. yazıda NSubstitute'un record'un ToString'iyle bütün alanları yazdığını görmüştük; argümanın okunur bir ToString'i olması iki kütüphanede de mesajın kalitesini belirliyor.

VerifyNoOtherCalls() doğrulanmamış hiçbir çağrı kalmamasını istiyor. stock.VerifyNoOtherCalls() "20 adetten az siparişte stoğa hiç dokunulmaz" testinde anlamlı. Ama sipariş deposunda kullanınca:

Moq.MockException : Mock<IOrderRepository:11>:
This mock failed verification due to the following unverified invocations:

   IOrderRepository.NextSequence(2026)

NextSequence bir sorgu; kurulduğu hâlde Verify edilmediği için "doğrulanmamış çağrı" sayılıyor. VerifyNoOtherCalls'u her mock'a eklemek, Önce Tahmin Et'teki strict mock'un aynısını başka bir yoldan yapmak demek.

Verifiable() ile işaretlenen setup'lar Verify() ile, bütün setup'lar VerifyAll() ile doğrulanıyor. Eski Moq testlerinde sık görülen bu iki çağrı, "kurduğun her şey kullanılmalı" diyor; sorgu kurulumları için bu genelde yanlış bir sözleşme.

Property'ler, As<T>() ve Protected()

Moq'ta property'lerin varsayılan davranışı NSubstitute ve FakeItEasy'den farklı. session.SetupProperty(s => s.LastOrderNumber) satırını kaldırınca servis property'yi set etti ama okurken null geldi:

Assert.Equal() Failure: Strings differ
Expected: "BS-2026-000042"
Actual:   null

Loose bir Moq mock'u property'ye yapılan atamayı saklamıyor; SetupProperty o property'yi gerçek bir auto-property gibi davranır hale getiriyor, SetupAllProperties() bunu bütün property'ler için yapıyor. NSubstitute ve FakeItEasy'de ise atanan değer varsayılan olarak geri okunuyor; FakeItEasy testimizde ek kurulum gerekmedi.

As<T>() mock'a ikinci bir interface ekliyor. Servis, depo IUnitOfWork'ü de uyguluyorsa kayıttan sonra commit ediyor:

[Fact]
public async Task Unit_of_work_is_committed_after_the_save()
{
    List<string> calls = [];
    orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
        .Callback(() => calls.Add("save"))
        .Returns(Task.CompletedTask);
    orders.As<IUnitOfWork>()
        .Setup(u => u.CommitAsync(It.IsAny<CancellationToken>()))
        .Callback(() => calls.Add("commit"))
        .Returns(Task.CompletedTask);

    await Place(new OrderLine(Dune, 20, 12.50m));

    Assert.Equal(["save", "commit"], calls);
}

orders.Object artık hem IOrderRepository hem IUnitOfWork. As<T>() mock'un .Object'i ilk kez kullanılmadan önce çağrılmalı; sonra çağırınca "Mock type has already been initialized by accessing its Object property. Adding interfaces must be done before that." exception'ı geliyor. Sırayı Callback ile bir listeye yazıp tek Assert.Equal ile kontrol etmek, MockSequence'tan (strict mock'la tasarlanmış) daha okunur.

Protected() protected member'ları kurmaya yarıyor. Template method pattern'iyle yazılmış bir export job'ı, alt class yazmadan test edelim:

[Fact]
public async Task Export_job_publishes_every_pending_order()
{
    IReadOnlyList<Order> pending =
    [
        new(Guid.NewGuid(), "BS-2026-000042", "library@example.org", [new OrderLine("9780441172719", 20, 11.25m)]),
        new(Guid.NewGuid(), "BS-2026-000043", "school@example.org", [new OrderLine("9780261103344", 30, 8.91m)]),
    ];
    Mock<OrderExportJob> job = new();
    job.Protected()
        .Setup<Task<IReadOnlyList<Order>>>("LoadPendingAsync", ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(pending);
    job.Protected()
        .Setup<Task>("PublishAsync", ItExpr.IsAny<Order>(), ItExpr.IsAny<CancellationToken>())
        .Returns(Task.CompletedTask);

    int published = await job.Object.RunAsync(TestContext.Current.CancellationToken);

    Assert.Equal(2, published);
    job.Protected().Verify(
        "PublishAsync", Times.Exactly(2), ItExpr.IsAny<Order>(), ItExpr.IsAny<CancellationToken>());
}

Protected member'lar lambda ile yazılamadığı için ad string olarak veriliyor ve matcher'lar It yerine ItExpr'den geliyor. Alışkanlıkla It.IsAny yazınca test, yanıltıcı bir mesajla kaldı: ArgumentException : Use ItExpr.IsNull<TValue> rather than a null argument value, as it prevents proper method lookup. (It.IsAny bu bağlamda null döndüğü için Moq onu null bir argüman olarak görüyor). Method adını değiştiren bir refactoring bu testi derleme anında değil çalışma anında kırıyor. Bu yüzden Protected()'ı mevcut bir hiyerarşiyi test altına almak için kullan; yeni kodda protected abstract method yerine enjekte edilen bir interface daha kolay test ediliyor.

Mevcut Moq Koduyla Yaşamak: Sürüm Sabitleme ve Eşleme Tablosu

07. yazıda Moq'un 2023'teki SponsorLink olayını anlattık. Yazım günü NuGet'teki durum: 4.20.0, 4.20.1 ve 4.20.2 listeden kaldırılmış (unlisted); aramada görünmüyorlar, ama bir proje tam sürümü istiyorsa hâlâ indirilebiliyorlar. Eski bir projede bu sürümlerden birini görürsen yükseltmeyi erteleme. 4.20.72 (Eylül 2024) uzun süre son sürümdü; 4.21.0 24 Eylül 2026'da geldi ve release notes'una göre ifade ile yazılan It.Is'in tek kez derlenmesi, nullable constructor argümanları ve çok sayıda büyük class'ı mock'larken performans iyileştirmesi getirdi. Lisansı BSD (kısıtlaması az, ticari kullanıma açık bir açık kaynak lisans ailesi) ailesinden BSD-3-Clause, bağımlılığı Castle.Core 5.2.1 (Apache-2.0). Moq'u kullanmaya devam eden bir projede sürümü central package management ile tam olarak sabitle:

<PackageVersion Include="Moq" Version="[4.21.0]" />

Köşeli parantez "tam olarak bu sürüm" demek; otomatik bağımlılık güncelleyicileri yükseltmeyi ayrı bir pull request olarak açıyor ve sürüm notunu okumadan birleştiremiyorsun.

Testleri yeniden yazmak için yeniden yazmak nadiren değer. Ama bir test zaten değişiyorsa ya da ekip NSubstitute'a geçmeye karar verdiyse, eşleme mekanik:

İşMoq 4.21NSubstitute 6.2FakeItEasy 9.0
Oluşturmanew Mock<T>() + .ObjectSubstitute.For<T>()A.Fake<T>()
DönüşSetup(x => x.M(a)).Returns(v)x.M(a).Returns(v)A.CallTo(() => x.M(a)).Returns(v)
Async dönüşReturnsAsync(v)Returns(v)Returns(v)
Ardışık dönüşSetupSequence(...).Returns(a).Returns(b)Returns(a, b)ReturnsNextFromSequence(a, b)
ExceptionThrows / ThrowsAsyncThrows / ThrowsAsyncThrows / ThrowsAsync
Her değerIt.IsAny<T>()Arg.Any<T>()A<T>._
KoşulluIt.Is<T>(p)Arg.Is<T>(p)A<T>.That.Matches(p)
out parametrekurulumdaki değişkencall[i] = vAssignsOutAndRefParameters(v)
Argüman yakalamaCallback(...)Arg.Do / When..DoInvokes(...)
Bir kez çağrıldıVerify(..., Times.Once())Received(1)MustHaveHappenedOnceExactly()
Hiç çağrılmadıTimes.Never()DidNotReceive()MustNotHaveHappened()
PropertySetupProperty gerekliotomatikotomatik
StrictMockBehavior.StrictyokA.Fake<T>(o => o.Strict())

Aynı kurumsal sipariş senaryosu FakeItEasy ile:

public InstitutionalCheckoutFakeTests()
{
    int available;
    A.CallTo(() => stock.TryReserve(A<string>._, A<int>._, out available))
        .Returns(true)
        .AssignsOutAndRefParameters(500);
    A.CallTo(() => prices.DiscountRate(A<CustomerAccount>.That.Matches(c => c.IsInstitutional), A<int>._))
        .Returns(0.10m);
    A.CallTo(() => clock.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
    A.CallTo(() => orders.NextSequence(2026)).ReturnsNextFromSequence(42, 43);
}

[Fact]
public async Task Bulk_order_is_saved_once_with_the_discount()
{
    await Place(new OrderLine(Dune, 20, 12.50m));

    A.CallTo(() => orders.SaveAsync(A<Order>.That.Matches(o => o.Total == 225.00m), A<CancellationToken>._))
        .MustHaveHappenedOnceExactly();
    Assert.Equal("BS-2026-000042", session.LastOrderNumber);
}

FakeItEasy'de .Object yok: A.Fake<T>() doğrudan T döndürüyor ve her kurulum A.CallTo ile başlıyor. out değeri AssignsOutAndRefParameters ile kurulumdan sonra veriliyor; session.LastOrderNumber ise hiçbir kurulum olmadan atanan değeri dönüyor. Üç kütüphane de çalışma anında aynı Castle.Core proxy'sini kullanıyor; Moq 4.21.0 ve FakeItEasy 9.0.1 Castle.Core 5.2.1'e, NSubstitute 6.2.0 5.1.1'e bağlı. FakeItEasy'nin lisansı MIT (kısıtlaması çok az, kısa bir açık kaynak lisansı).

Üretimde Dikkat

Moq'un hata exception'ları xUnit v3'e assertion hatası olarak görünmüyor. Çıktıda Xunit.MicrosoftTestingPlatform.XunitException: Moq.MockException : ... biçiminde, beklenmeyen bir exception gibi raporlanıyor (14. yazıdaki IAssertionException bölümü); NSubstitute'un ReceivedCallsException'ı da öyle. Test sonuçlarını hata türüne göre ayıran bir CI raporu kullanıyorsan, mock doğrulamalarının "exception" altında görüneceğini bil.

Legacy testi sadeleştirmeyi tek seferde yapma. Bir test projesinde yüzlerce strict mock varsa, önce MockRepository'nin davranışını loose'a çevir ve kırmızıya dönen testleri tek tek incele: loose'da kalan bir test, gerçekten bir davranışın değiştiğini gösteriyor olabilir. Ardından VerifyAll çağrılarını, yalnız sistem dışına etkisi olan çağrıları doğrulayan Verify'lerle değiştir.

Constructor'daki bağımlılık sayısına bak. InstitutionalCheckout altı bağımlılık alıyor ve legacy testteki kurulumun yarısı bunlarla ilgili. Bu, testin değil tasarımın sorunu; servisi bölmek (stok rezervasyonunu ayrı bir domain service'e almak gibi) Refactoring serisinin konusu.

Sık Düşülen Tuzaklar

Her testte VerifyAll ile implementasyonu kilitlemek. Sorgu kurulumlarını doğrulamak, 06. yazıdaki "stub'a assert etmek" hatasının Moq hâli: servis aynı sonucu daha az sorguyla üretmeye başladığında test kırılıyor.

Strict mock'un küçük refactoring'de bütün testleri kırması. Önce Tahmin Et'teki log satırı tek bir testi değil, o servisin strict mock kullanan bütün testlerini kırar. Strict davranışı gerçekten "bu çağrı asla olmamalı" demek istediğin tek yerde bırak; onun için de Times.Never() çoğu zaman yeterli.

ILogger'ı Moq ile doğrulamak. logger.Verify(l => l.LogInformation(...)) yazınca test NotSupportedException : Unsupported expression: l => l.LogInformation(It.IsAny<string>(), It.IsAny<object[]>()) Extension methods (here: LoggerExtensions.LogInformation) may not be used in setup / verification expressions. ile kalıyor. Extension method'lar mock'lanamaz; ILogger.Log<TState>'i doğrudan doğrulamak mümkün ama okunması zor. Log'u doğrulaman gerekiyorsa FakeLogger kullan.

Test projesinin namespace'inde Moq geçmesi. Bu yazının test projesine önce Bookstore.Ordering.Moq.Tests adını verdim ve derleme error CS0234: The type or namespace name 'Range' does not exist in the namespace 'Bookstore.Ordering.Moq' ile durdu: namespace içindeki Moq.Range, kütüphaneyi değil kendi namespace'imizi gösterdi. Proje adını Bookstore.Ordering.Legacy.Tests yaptım; aynı sorun FakeItEasy için de geçerli.

Küçük Bir Deneme

★☆☆ Önce Tahmin Et'teki legacy testi projene ekle ve MockBehavior.Strict'i MockBehavior.Loose yap. Test geçiyor mu? Sonra mocks.VerifyAll()'u silmeden InstitutionalCheckout'tan prices.DiscountRate çağrısını kaldırıp (indirimi sabit 0.10m yaparak) çalıştır: ne oldu?

★★☆ Short_stock_is_reported_with_the_available_count'u iki satırlı bir siparişe genişlet: Dune yeterli, The Hobbit yetersiz. İkinci satır için ayrı bir out değeriyle Setup yaz ve kaydın hiç yapılmadığını doğrula.

★★★ InstitutionalCheckoutTests'i NSubstitute'a taşı. Tablodaki eşlemeyle kaç satır değişti, hangi testte eşleme yetmedi (SetupProperty, As<T>(), Mock.Of gibi)?

Bir sonraki yazıda HttpClient kullanan kodu unit test ediyoruz: HttpMessageHandler stub'ı, RichardSzalay.MockHttp, IHttpClientFactory ve typed client; Shipping module'ünün kargo takip client'ı üzerinden.