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

Bogus ve AutoFixture Rehberi: Faker<T> Kuralları, Türkçe Locale, Seed, Strict Mode, AutoData ve Customization

Mert Özen 15 Kas 2026 14 dk 4 görüntülenme
Bogus ve AutoFixture Rehberi: Faker<T> Kuralları, Türkçe Locale, Seed, Strict Mode, AutoData ve Customization

Bogus'ta seed veriyi değil rastgele sayı dizisini sabitler: Faker'ın ortasına eklenen tek bir RuleFor, ondan sonraki her değeri değiştirir; bu yüzden üretilen değerin kendisi değil, kurallara uyduğu assert edilir. RuleFor, CustomInstantiator, RuleSet, StrictMode, GenerateLazy, Türkçe locale ile bulunan gerçek bir arama hatası; AutoFixture'da DateOnly customization'ı, Build tuzağı, AutoData ve AutoNSubstitute.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3, Bogus 35.6.5, AutoFixture 4.18.1, AutoFixture.Xunit3 4.19.0 ve AutoFixture.AutoNSubstitute 4.18.1 ile derlenip denenmiştir.

Önce Tahmin Et

10. yazıda Bogus ve AutoFixture'ı birer bölümle tanıttık: biri gerçekçi, diğeri anonim veri üretiyordu ve ikisinde de seed'in önemini konuştuk. Bu yazı ikisinin API'sine ayrıntılı bakıyor. Örnekler, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Catalog module'ünden: Book, ISBN (International Standard Book Number; kitabın 13 haneli uluslararası kimlik numarası) doğrulayan Isbn13 ve yazara göre arama yapan BookSearch. Katalog testleri için bir Faker yazdık:

public static Faker<Book> Create(int seed, string locale = "en") =>
    new Faker<Book>(locale)
        .UseSeed(seed)
        .UseDateTimeReference(Reference)
        .StrictMode(true)
        .RuleFor(b => b.Isbn, f => NewIsbn(f))
        .RuleFor(b => b.Title, f => f.Lorem.Sentence(f.Random.Int(1, 4)).TrimEnd('.'))
        .RuleFor(b => b.Author, f => f.Name.FullName())
        .RuleFor(b => b.Pages, f => f.Random.Int(80, 900))
        .RuleFor(b => b.Price, (f, b) => Math.Round(5m + b.Pages * 0.04m + f.Random.Decimal(0m, 10m), 2))
        .RuleFor(b => b.PublishedOn, f => f.Date.PastDateOnly(40))
        .RuleFor(b => b.Language, _ => locale);

Ve seed sabit olduğu için ilk kitabın değerlerine güvenen bir test:

[Fact]
public void First_book_of_the_seeded_catalog()
{
    Book first = BookFaker.Create(seed: 2026).Generate();

    Assert.Equal("9782999068636", first.Isbn);
    Assert.Equal(173, first.Pages);
    Assert.Equal(17.14m, first.Price);
}

Test yeşil. Sonra Book'a isteğe bağlı bir Subtitle property'si eklendi ve Faker'a, Title'ın hemen altına bir kural yazıldı:

.RuleFor(b => b.Subtitle, f => f.Random.Bool(0.3f) ? f.Lorem.Sentence(3).TrimEnd('.') : null)

Seed hâlâ 2026. Test geçer mi?

Cevap: Seed Sabit, Sıra Değil

Kalıyor:

failed Bookstore.Catalog.Tests.SeededCatalogTests.First_book_of_the_seeded_catalog (29ms)
  Assert.Equal() Failure: Values differ
  Expected: 173
  Actual:   508

ISBN satırı geçti, sayfa sayısı kaldı. UseSeed(2026) Faker'ın rastgele sayı üretecini sabitliyor; her kural sırayla bu üreteçten sayı çekiyor. Yeni kural Title'dan sonra iki sayı daha çektiği için ondan sonraki her kural (Author, Pages, Price, PublishedOn) dizinin farklı bir yerinden okumaya başladı. Önceki kurallar (Isbn, Title) aynı kaldı.

Bogus'un README'si iki öneri veriyor: yeni kuralları sona ekle ve üretilen değerin kendisini değil, kurallara uyduğunu assert et. İlkini uyguladık; kural en alta taşınınca bu test tekrar yeşile döndü. Ama sorunun kökü testte: testin adı "ilk kitap" diyor, ama gerçekte Bogus'un iç sırasını doğruluyor. Bogus'un major sürümü değişince de (README'ye göre locale verisinin güncellenmesi deterministik çıktıyı değiştirebiliyor) aynı test kırılırdı. Yerine yazdıklarımız:

[Fact]
public void Same_seed_gives_the_same_catalog()
{
    List<Book> first = BookFaker.Create(seed: 2026).Generate(100);
    List<Book> second = BookFaker.Create(seed: 2026).Generate(100);

    Assert.Equal(first, second);
}

[Fact]
public void Catalog_of_1000_books_has_valid_and_unique_isbns()
{
    List<Book> catalog = BookFaker.Create(seed: 2026).Generate(1_000);

    Assert.All(catalog, b => Assert.True(Isbn13.IsValid(b.Isbn), $"{b.Isbn} is not a valid ISBN-13"));
    Assert.Distinct(catalog.Select(b => b.Isbn));
}

[Fact]
public void Generated_values_stay_inside_the_rules()
{
    List<Book> catalog = BookFaker.Create(seed: 7).Generate(1_000);

    Assert.All(catalog, b =>
    {
        Assert.InRange(b.Pages, 80, 900);
        Assert.InRange(b.Price, 5m + b.Pages * 0.04m, 15m + b.Pages * 0.04m);
        Assert.InRange(b.PublishedOn, new DateOnly(1986, 11, 1), new DateOnly(2026, 11, 1));
    });
}

Seed hâlâ önemli: kırmızı bir koşuyu aynı veriyle yeniden üretmeni sağlıyor. Ama testin doğruladığı şey üretilen verinin kurallara uyması, belirli bir değer değil.

Faker<T>: RuleFor, Bağımlı Kurallar ve CustomInstantiator

RuleFor(b => b.Property, f => ...) bir property'nin nasıl doldurulacağını tanımlıyor; f, Bogus'un veri setlerine (Name, Address, Internet, Lorem, Date, Random…) açılan Faker object'i. İki parametreli hâli (f, b) => ... o ana kadar doldurulmuş object'i de veriyor: Price kuralı sayfa sayısına bağlı, bu yüzden Pages'ten sonra yazıldı. Bağımlı bir kuralı bağımlı olduğu kuraldan önce yazarsan, b.Pages henüz 0'dır ve hata vermez; yalnız yanlış veri üretir.

Geçerli bir ISBN-13 için Bogus'un hazır bir method'u yok (Commerce.Ean13() 13 haneli bir EAN (European Article Number; ürün barkodlarında kullanılan numara standardı) üretiyor, 978/979 önekini garanti etmiyor). Kontrol hanesini Catalog'daki Isbn13'ten alıyoruz; test verisi, production'ın doğrulama kuralını kullanıyor:

public static string NewIsbn(Faker f)
{
    string first12 = f.PickRandom("978", "979") + f.Random.ReplaceNumbers("#########");
    return first12 + Isbn13.CheckDigit(first12);
}

Tarihler için iki ayrıntı var. Date.PastDateOnly(40) .NET'in DateOnly tipini doğrudan üretiyor. Seed tarihi sabitlemiyor, çünkü "geçmiş" bugüne göre hesaplanıyor; UseDateTimeReference(new DateTime(2026, 11, 1, ...)) referans tarihi sabitlemezse aynı seed yarın başka tarihler üretir.

Book init property'leri olan bir record olduğu için RuleFor doğrudan çalışıyor. Constructor'dan başka yolu olmayan positional record'lar için CustomInstantiator:

Faker<CustomerAccount> customers = new Faker<CustomerAccount>("tr")
    .UseSeed(2026)
    .CustomInstantiator(f => new CustomerAccount(
        f.Internet.ExampleEmail(),
        IsInstitutional: f.Random.Bool(0.2f),
        CompletedOrders: f.Random.Int(0, 40)));

İlk üç kayıt:

CustomerAccount { Email = Atsiz_Cetin7@example.org, IsInstitutional = False, CompletedOrders = 35 }
CustomerAccount { Email = Bagaturipi40@example.com, IsInstitutional = False, CompletedOrders = 5 }
CustomerAccount { Email = Bayri_Arslanoglu@example.org, IsInstitutional = True, CompletedOrders = 36 }

Internet.ExampleEmail() yalnız example.com, example.org ve example.net domain'lerini kullanıyor. Internet.Email() ise gerçek ücretsiz e-posta servislerinin domain'lerini üretiyor; denemede gmail ve hotmail adresleri geldi. Test verisi log'a, hata mesajına, ekran görüntüsüne düşüyor; gerçekte var olabilecek bir adresi orada görmek istemezsin. Test, bunu kural olarak sabitliyor: 200 hesabın hepsinin e-postası example domain'inde.

RuleSet, StrictMode, GenerateLazy ve FinishWith

RuleSet adı olan bir kural grubu tanımlıyor; Generate'e verilen isimlere göre varsayılan kuralların üzerine yazıyor:

.RuleSet("classic", rules => rules
    .RuleFor(b => b.PublishedOn, f => f.Date.PastDateOnly(150, new DateOnly(1950, 1, 1))));
[Fact]
public void Classic_rule_set_moves_publication_before_1950()
{
    List<Book> classics = BookFaker.Create(seed: 2026).Generate(50, "default,classic");

    Assert.All(classics, b => Assert.True(b.PublishedOn < new DateOnly(1950, 1, 1)));
}

"default" adı ayrıca yazılmalı. Yalnız "classic" verince RuleSet dışındaki kurallar hiç çalışmadı: kitapların ISBN'i, başlığı ve yazarı boş string, fiyatı 0 geldi ve strict mode bunu yakalamadı.

StrictMode(true), kuralı olmayan bir property kaldığında Generate'i durduruyor. Önce Tahmin Et'teki Subtitle property'si eklenip kuralı yazılmasaydı test şöyle kalırdı (burada eksik kuralları göstermek için bilerek yarım bırakılmış bir Faker):

Bogus.ValidationException : Validation was called to ensure all properties / fields have rules.
There are missing rules for Faker<T> 'Book'.
=========== Missing Rules ===========
Subtitle
Price
Pages
PublishedOn
Language

Strict mode olmadan yeni property sessizce null ya da 0 kalır ve o alana dayanan testler gerçek veri yerine boş değerle çalışır. Paylaşılan Faker'larda açık tutmak, domain değiştiğinde Faker'ın da güncellenmesini zorunlu kılıyor.

Generate(n) bir List<T> döndürüyor; GenerateLazy(n) ise object'leri ancak enumerate edildikçe üretiyor. FinishWith bütün kurallar çalıştıktan sonra her object için bir kez çağrılıyor; burada üretilen object'leri saymak için:

[Fact]
public void Lazy_generation_creates_only_what_is_enumerated()
{
    int created = 0;
    Faker<Book> books = BookFaker.Create(seed: 2026).FinishWith((_, _) => created++);

    List<Book> firstFive = [.. books.GenerateLazy(1_000).Take(5)];

    Assert.Equal(5, firstFive.Count);
    Assert.Equal(5, created);
}

1.000 yerine 5 kitap üretildi. Büyük veriyle performans denemesi yaparken ya da bir stream'i beslerken GenerateLazy belleği korur.

Türkçe Locale: Gerçek Veriyle Çıkan Hata

new Faker<Book>("tr") isim, adres, şehir ve kelime verisini Türkçe setten alıyor. Aynı seed ile üretilen iki kitap:

Book { Isbn = 9782999068636, Title = Dışarı sıradanlıktan illo öyle, Subtitle = Qui açılmadan ipsam, Author = Aydoğdu Korol, Price = 17.14, Pages = 173, PublishedOn = 7/3/2018, Language = tr }
Book { Isbn = 9786801711079, Title = Odit, Subtitle = , Author = Atberilgen Alpuğan, Price = 21.26, Pages = 325, PublishedOn = 2/13/2021, Language = tr }

ISBN, fiyat ve sayfa sayısı İngilizce katalogla aynı: aynı seed aynı sayıları üretiyor, locale yalnız metin verisini değiştiriyor. Başlıklarda Latince ve Türkçe kelimeler karışık geliyor; README'ye göre bir locale'de eksik olan veri seti İngilizceden tamamlanıyor.

Bu veriyi BookSearch üzerinde kullanınca bir hata çıktı. İlk hâli Contains(term, StringComparison.OrdinalIgnoreCase) ile arıyordu. Test, 1.000 Türkçe kitabın her yazarını, kullanıcının yazacağı gibi küçük harfle arıyor:

[Fact]
public void Every_turkish_author_is_found_by_a_lowercase_search()
{
    List<Book> catalog = BookFaker.Create(seed: 2026, locale: "tr").Generate(1_000);
    BookSearch search = new(catalog);

    Assert.All(catalog.Select(b => b.Author).Distinct(), author =>
        Assert.NotEmpty(search.ByAuthor(author.ToLower(Turkish))));
}
Assert.All() Failure: 38 out of 998 items in the collection did not pass.
[42]:  Item:  "İlgegü Düşenkalkar"
       Error: Assert.NotEmpty() Failure: Collection was empty
[55]:  Item:  "İdil Özdenak"
       Error: Assert.NotEmpty() Failure: Collection was empty
[136]: Item:  "İkeme Sarıoğlu"
       Error: Assert.NotEmpty() Failure: Collection was empty

38 yazarın hepsinin adında İ ya da I var. Ordinal karşılaştırma Türkçe büyük-küçük harf kurallarını bilmiyor: "idil" ile "İdil", "ışıl" ile "Işıl" eşleşmiyor. Sadece tr-TR culture'ına geçmek de çözüm değil; o zaman "isaac" araması "Isaac Asimov"u bulamıyor, çünkü Türkçede i'nin büyüğü İ. Düzeltme, karşılaştırma kuralını kitabın diline bağlamak:

// Büyük-küçük harf kuralı kitabın diline bağlı: "İdil", "idil" ile yalnız Türkçe kurallarla eşleşir
public IReadOnlyList<Book> ByAuthor(string term) =>
[
    .. books
        .Where(b => CultureInfo.GetCultureInfo(b.Language).CompareInfo
            .IndexOf(b.Author, term, CompareOptions.IgnoreCase) >= 0)
        .OrderBy(b => b.Title),
];

Karışık bir katalogda İngilizce yazarların hâlâ bulunduğunu ikinci bir test kontrol ediyor. "Ada Lovelace" gibi elle yazılmış birkaç isimle bu hata hiç görünmezdi; gerçekçi verinin değeri tam olarak bu.

AutoFixture: Create, Build ve Customization

AutoFixture'ın stabil sürümü hâlâ 4.18.1 (Kasım 2023). Book'u ilk kez oluşturmak istediğimizde kaldı:

AutoFixture.ObjectCreationExceptionWithPath : AutoFixture was unable to create an instance from System.DateOnly because creation unexpectedly failed with exception. Please refer to the inner exception to investigate the root cause of the failure.

Request path:
	Bookstore.Catalog.Book
	  System.DateOnly PublishedOn
	    System.DateOnly

Inner exception messages:
	System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
	  System.ArgumentOutOfRangeException: Year, Month, and Day parameters describe an un-representable DateTime.

4.18.1 DateOnly'yi tanımıyor, onu sıradan bir struct gibi constructor'ına rastgele yıl, ay ve gün vererek oluşturmaya çalışıyor. Mesajın "Request path" kısmı hangi property'de takıldığını gösteriyor. Ayrı bir projede 5.0.0-rc.1 (31 Temmuz 2026) ile denediğimde DateOnly ek ayar gerekmeden üretildi; ama RC (yayın öncesi son aday sürüm) bir paketi snapshot'a koymadık. Çözüm bir customization:

// AutoFixture 4.18.1 DateOnly'yi kendisi oluşturamıyor: constructor'a rastgele yıl/ay/gün veriyor
public sealed class DateOnlyCustomization : ICustomization
{
    public void Customize(IFixture fixture) =>
        fixture.Customize<DateOnly>(c => c.FromFactory((DateTime d) => DateOnly.FromDateTime(d)));
}

// Anonim kitapların da geçerli bir ISBN'i ve gerçek bir culture adı olmalı
public sealed class BookCustomization : ICustomization
{
    public void Customize(IFixture fixture) =>
        fixture.Customize<Book>(c => c
            .With(b => b.Isbn, (int n) => ToIsbn(n))
            .With(b => b.Language, "en"));

    private static string ToIsbn(int n)
    {
        string first12 = "978" + (n % 1_000_000_000).ToString("D9");
        return first12 + Isbn13.CheckDigit(first12);
    }
}

public sealed class BookstoreCustomization() : CompositeCustomization(new DateOnlyCustomization(), new BookCustomization());

ICustomization bir kuralı tek yerde toplamanın yolu; CompositeCustomization birkaçını birleştiriyor. FromFactory((DateTime d) => ...) AutoFixture'a "önce bir DateTime üret, onu dönüştür" diyor. Customize edilmiş fixture'ın ürettiği kitap:

Book { Isbn = 9780000001078, Title = Title5091d1e5-1556-45d1-ae5e-ef397ff1bbfa, Subtitle = Subtitleea847e9a-0a49-4a55-8404-11a5c3e16821, Author = Authoreef4d46f-cb24-4e35-a864-e4fa72b14dbd, Price = 81, Pages = 17, PublishedOn = 5/13/2028, Language = en }

Yayın tarihi 2028, sayfa sayısı 17: anonim veri gerçekçi olmaya çalışmıyor. Değerin önemli olduğu yerde test onu kendisi veriyor:

[Fact]
public void Build_sets_only_what_the_test_cares_about()
{
    Book dune = fixture.Build<Book>()
        .With(b => b.Title, "Dune")
        .With(b => b.Author, "Frank Herbert")
        .With(b => b.Language, "en")
        .Without(b => b.Subtitle)
        .Create();
    BookSearch search = new([dune, .. fixture.CreateMany<Book>(20)]);

    Assert.Equal([dune], search.ByAuthor("herbert"));
}

.With(b => b.Language, "en") satırı ilk denemede yoktu ve test kaldı:

Xunit.MicrosoftTestingPlatform.XunitException: System.Globalization.CultureNotFoundException : Culture is not supported. (Parameter 'name')
language42fb2f2e-42ac-40ae-bd3d-b156f3f30dce is an invalid culture identifier.

Build<T>(), o tip için yapılmış Customize<T> ayarlarını kullanmıyor; sıfırdan bir kurulum başlatıyor. DateOnlyCustomization çalışmaya devam etti (o, DateOnly tipine ait), ama BookCustomization'ın ISBN ve dil kuralları devreden çıktı. Build kullanırken tipin customization'ına güvenme, gereken değerleri With ile ver.

[AutoData], [InlineAutoData] ve AutoNSubstitute

xUnit v3 entegrasyonu ayrı bir paket: AutoFixture.Xunit3. Stabil 4.19.0 (Nisan 2025) xunit.v3.extensibility.core 1.0.0 ve üstüne bağlı ve xUnit 4.0.3 ile sorunsuz çalıştı; 5.0.0-rc.1 sürümü .NET 8 ve .NET 10 için derlenmiş. Paketlerin hepsinin lisansı MIT (kısıtlaması çok az, kısa bir açık kaynak lisansı). Customization'ları her testte tekrar yazmamak için kendi attribute'unu türet:

public sealed class BookstoreAutoDataAttribute() : AutoDataAttribute(CreateFixture)
{
    internal static IFixture CreateFixture() =>
        new Fixture().Customize(new BookstoreCustomization()).Customize(new AutoNSubstituteCustomization());
}

public sealed class BookstoreInlineAutoDataAttribute(params object[] values)
    : InlineAutoDataAttribute(BookstoreAutoDataAttribute.CreateFixture, values);

[BookstoreAutoData] test method'unun bütün parametrelerini fixture'dan dolduruyor; [BookstoreInlineAutoData(20)] ilk parametreleri verilen değerlerle, kalanları fixture'la. AutoNSubstituteCustomization, interface ve abstract class isteyen her yere bir NSubstitute sahtesi koyuyor; [Frozen] aynı instance'ı hem teste hem test edilen koda veriyor. Altı bağımlılığı olan InstitutionalCheckout (17. yazı) tek parametreyle oluşuyor:

[Theory]
[BookstoreInlineAutoData(20)]
[BookstoreInlineAutoData(150)]
public async Task Institutional_order_is_saved_with_every_copy(
    int copies,
    [Frozen] IStockService stock,
    [Frozen] IOrderRepository orders,
    InstitutionalCheckout sut)
{
    stock.TryReserve(default!, default, out _).ReturnsForAnyArgs(true);
    (CustomerAccount customer, IReadOnlyList<OrderLine> lines) =
        new InstitutionalOrderBuilder().WithCopies(copies).AcrossTitles(3).Build();

    await sut.PlaceAsync(customer, lines, TestContext.Current.CancellationToken);

    await orders.Received(1).SaveAsync(
        Arg.Is<Order>(o => o.Lines.Sum(l => l.Quantity) == copies), Arg.Any<CancellationToken>());
}

Bir sürpriz: ILogger<InstitutionalCheckout> bir NSubstitute sahtesi, ama abstract TimeProvider değil. Fixture'dan TimeProvider isteyince SystemTimeProvider, yani gerçek saat geldi; sipariş numarasındaki yıl testin çalıştığı yıla göre değişiyor. Bu test numarayı kontrol etmediği için sorun yok; numarayı kontrol eden bir test 11. yazıdaki FakeTimeProvider'ı fixture.Inject(...) ile vermeli. Testin umursamadığı bağımlılıkların görünmemesi AutoNSubstitute'un faydası; aynı zamanda riski: hangi bağımlılığın neyle doldurulduğu imzadan anlaşılmıyor. [Frozen] parametreleri açık yazmak, testin "önemli olanlar bunlar" demesi.

Sürüm uyarısı 10. yazıdan beri aynı: AutoFixture.AutoNSubstitute 4.18.1 NSubstitute'u < 6.0.0 ile sınırlıyor, projede 6.2.0 var ve restore NU1608 uyarısı veriyor. 5.0.0-rc.1'de aralık [5.0.0, 7.0.0). Stabil sürümde kalıp uyarıyı bilinçli olarak kabul ettik, ama proje genelinde değil, yalnız NSubstitute referansında:

<!-- AutoNSubstitute 4.18.1 NSubstitute'u < 6.0.0 ile sınırlıyor; 6.2.0 burada çalışıyor, NU1608 bilerek kabul edildi -->
<PackageReference Include="NSubstitute" NoWarn="NU1608" />

NoWarn'ı AutoNSubstitute referansına koymak işe yaramadı; uyarı, sürümü aralığın dışında kalan pakete (NSubstitute) bağlanıyor.

Test Data Builder ile Birlikte

10. yazıdaki Test Data Builder anlamlı varsayılanları taşıyor, Bogus ise testin umursamadığı alanları gerçekçi değerlerle dolduruyor. Kurumsal sipariş (en az 20 adet) için:

// Anlamlı varsayılanlar builder'dan, gerçekçi dolgu (ISBN, fiyat) Bogus'tan
public sealed class InstitutionalOrderBuilder
{
    private CustomerAccount customer = new("library@example.org", IsInstitutional: true, CompletedOrders: 3);
    private int copies = InstitutionalCheckout.MinimumQuantity;
    private int titles = 2;

    public InstitutionalOrderBuilder WithCopies(int total)
    {
        copies = total;
        return this;
    }

    public InstitutionalOrderBuilder AcrossTitles(int count)
    {
        titles = count;
        return this;
    }

    public InstitutionalOrderBuilder ForIndividualCustomer()
    {
        customer = customer with { IsInstitutional = false };
        return this;
    }

    public (CustomerAccount Customer, IReadOnlyList<OrderLine> Lines) Build()
    {
        List<Book> books = BookFaker.Create(seed: 20).Generate(titles);
        OrderLine[] lines =
        [
            .. books.Select((b, i) => new OrderLine(b.Isbn, copies / titles + (i < copies % titles ? 1 : 0), b.Price)),
        ];
        return (customer, lines);
    }
}

Varsayılan sipariş iki kitaptan 10'ar adet; minimum sınırın tam üstünde. Sınırı test eden test yalnız umursadığı değeri söylüyor:

[Theory, BookstoreAutoData]
public async Task Nineteen_copies_are_rejected(InstitutionalCheckout sut)
{
    (CustomerAccount customer, IReadOnlyList<OrderLine> lines) = new InstitutionalOrderBuilder().WithCopies(19).Build();

    InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(
        () => sut.PlaceAsync(customer, lines, TestContext.Current.CancellationToken));

    Assert.Equal("Institutional orders need at least 20 copies; got 19.", ex.Message);
}

Üç aracın iş bölümü: builder testin ne anlattığını, Bogus verinin gerçekçiliğini, AutoFixture testin umursamadığı bağımlılıkları üstleniyor. Catalog test projesinde 15 test var; 1.000'er kitaplık testler dahil hepsi bir saniyenin altında bitiyor.

Üretimde Dikkat

Global seed kullanma. Randomizer.Seed = new Random(...) bütün Faker'ların paylaştığı static bir kaynak. xUnit 4.0'da testler paralel çalışırken (05. yazı) hangi testin diziden hangi sayıyı çektiği çalışma sırasına bağlı oluyor; seed'i sabitlemek veriyi sabitlemiyor. Her Faker'da UseSeed kullan.

Bogus'u major sürüm atlayarak yükseltirken literal değer bekleyen testleri gözden geçir. README, deterministik çıktının değişmesini breaking change sayıyor ve bunu genelde release notes'ta belirtiyor; aynı major sürüm içinde kalmak çıktıyı koruyor.

Bogus verisini production seed'inde kullanma. Demo ortamı için üretilen 1.000 sahte müşteri, gerçek kullanıcıların arasına karışırsa ayırt edilemez; hele Email() gerçek domain'ler üretiyorsa bu adreslere e-posta bile gidebilir. Bogus'u test projesinde ve ayrı bir demo veri aracında tut, Bookstore.Api'nin referanslarına girmesin.

Sık Düşülen Tuzaklar

Seed'siz rastgele veri. 10. yazının Önce Tahmin Et'i bunu gösterdi: 30 koşudan 6'sı kırmızı. Kırmızı bir koşuyu yeniden üretemiyorsan, testi düzeltemezsin.

Üretilen değeri literal olarak assert etmek. Önce Tahmin Et'teki test; seed sabit olsa bile kural sırası ya da Bogus sürümü değişince kırılır.

Build<T>()'nin customization'ı atladığını bilmemek. Yukarıdaki CultureNotFoundException; hata başka bir yerde, ilgisiz bir mesajla çıkıyor.

AutoData ile testin niyetini gizlemek. Her parametre fixture'dan gelince test, hangi değerin sonucu belirlediğini söylemiyor; TimeProvider örneğindeki gibi bir bağımlılığın gerçek mi sahte mi olduğu bile görünmüyor. Sonuca etki eden değeri InlineAutoData ile, builder'la ya da test içinde açıkça ver.

Küçük Bir Deneme

★☆☆ BookFaker'a bir "bestseller" RuleSet'i ekle: fiyat 30–60 arası, yayın tarihi son iki yıl. Generate(20, "default,bestseller") ile test et; "default"'u unutunca ne oluyor?

★★☆ Every_turkish_author_is_found_by_a_lowercase_search'ü kitap başlıkları için de yaz (BookSearch.ByTitle). Türkçe locale'in başlık verisi yarı Latince; bu testin değeri yazarlardaki kadar yüksek mi?

★★★ Catalog test projesini AutoFixture 5.0.0-rc.1'e (AutoFixture, AutoFixture.Xunit3, AutoFixture.AutoNSubstitute) yükselt. DateOnlyCustomization hâlâ gerekli mi, NU1608 kayboldu mu, hangi test davranış değiştirdi?

Bir sonraki yazıda başka bir test framework'üne geçiyoruz: NUnit 5. xUnit bilen biri için constraint model, TestCase/TestCaseSource, SetUp/OneTimeSetUp, paralellik ve 27 Eylül 2026'da çıkan 5.0'ın breaking change'leri; eski bir NUnit 4 projesini 5.0'a yükselterek.