Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Modular Monolith — #07

Module Sınırlarını Korumak: Architecture Test'leri, Build Kuralları ve Module Bazlı Test

Mert Özen 7 Kas 2026 11 dk 4 görüntülenme
Module Sınırlarını Korumak: Architecture Test'leri, Build Kuralları ve Module Bazlı Test

Module sınırları dört seviyede korunur: derleyici (internal), build (Directory.Build.targets içinde Core'dan Core'a referansı durduran MSBuild kontrolü), test (ArchUnitNET ile parametrik kurallar) ve inceleme (CODEOWNERS). Kuralların ilk koşuda unutulmuş bir public type'ı, Contracts'a eklenen mutable bir DTO'yu ve method gövdesindeki Core bağımlılığını yakaladığını gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar C# 14, .NET 10, ArchUnitNET 0.13.5 ve xUnit v3 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'da aynı hafta üç pull request açılıyor. Birincisi, Ordering'in Bookstore.Ordering.Core project'ine Bookstore.Catalog.Core için bir project reference ekliyor. İkincisi, Catalog'un Contracts project'ine "Ordering'in ekranı da kullansın" diye get; set; property'li sıradan bir BookDto class'ı ekliyor. Üçüncüsü, dördüncü yazıda demo verisi için yazılmış DemoIds class'ına hiç dokunmuyor; o class zaten aylardır public duruyor.

Hangisini derleyici yakalar? Hangisini bir test? Hangisi kimsenin fark etmeden production'a gider? 03. yazıda kurduğumuz internal ve project reference düzeni bu üçünün kaçını durdurur?

Dört Seviyede Bekçi

Module sınırları, onları koruyan bir mekanizma yoksa zamanla aşınır. Mimariye Giriş serisinin "Mimariyi Zaman İçinde Korumak: Architecture Erosion ve Fitness Function'lar" yazısında bunun adını koymuş ve fitness function kavramını anlatmıştık; Clean/Onion/Hexagonal serisinin "Clean Architecture'ı Test Etmek ve Korumak: Core Test'leri, Adapter Test'leri ve Architecture Test'leri" yazısı da ArchUnitNET'in temel kullanımını ve Dependency Rule test'lerini gösterdi. Bu yazı yalnız module'e özgü kurallara odaklanıyor. Modular monolith'te bekçiler dört seviyede çalışıyor:

SeviyeAraçYakaladığıYakalayamadığı
Derlemeinternal, project referenceBaşka module'ün internal type'ına erişimYanlış project reference'ın eklenmesi
BuildDirectory.Build.targets içinde MSBuild kontrolüCore'dan başka bir Core'a project referenceAynı project içindeki tasarım kuralları
TestArchUnitNETType seviyesinde bağımlılıklar, public yüzey, Contracts'ın şekliİnsan kararı gerektiren değişiklikler
İncelemeCODEOWNERS ve branch protectionContracts ve kural değişikliklerinin doğru kişilere gitmesiKimsenin bakmadığı onay

Build Seviyesi: Project Reference Kuralı

Üçüncü yazıda dotnet add reference komutunun Ordering'den Catalog'un Core'una referansı hiç itirazsız eklediğini görmüştük. Bunu build'in kendisinde durdurmak için solution kökündeki Directory.Build.targets dosyasına küçük bir MSBuild target'ı ekledik:

<Project>
  <!-- Bir module'ün Core project'i başka bir module'ün Core project'ine referans veremez -->
  <Target Name="CheckModuleBoundaries" BeforeTargets="ResolveProjectReferences"
          Condition="$(MSBuildProjectName.StartsWith('Bookstore.')) And $(MSBuildProjectName.EndsWith('.Core'))">
    <ItemGroup>
      <ForbiddenCoreReference Include="@(ProjectReference)"
          Condition="$([System.String]::Copy('%(Filename)').EndsWith('.Core'))" />
    </ItemGroup>
    <Error Condition="'@(ForbiddenCoreReference)' != ''"
           Code="BOOKSTORE001"
           Text="$(MSBuildProjectName) references @(ForbiddenCoreReference->'%(Filename)'). Modules may only reference another module's Contracts project." />
  </Target>
</Project>

Directory.Build.targets, Directory.Build.props gibi klasör ağacında aranıp her project'e eklenir, ama project dosyasının sonunda; böylece ProjectReference item'larının hepsi tanımlanmış olur. Target, adı Bookstore. ile başlayıp .Core ile biten her project'te çalışıyor ve referanslar arasında başka bir .Core project'i varsa build'i durduruyor. İlk pull request'in CI'daki (Continuous Integration) hâli:

Directory.Build.targets(9,5): error BOOKSTORE001: Bookstore.Ordering.Core references Bookstore.Catalog.Core. Modules may only reference another module's Contracts project.

Hata kodu bizim tanımladığımız BOOKSTORE001. Mesaj sorunu ve çözümü birlikte söylüyor: Contracts'a referans ver. Bu kontrol test yazmadan, her dotnet build'de ve IDE'deki (Integrated Development Environment) her build'de çalışıyor; geri bildirim saniyeler içinde geliyor.

ArchUnitNET ile Module Kuralları

Project reference'ı yasaklamak yetmiyor: sınır bir project'in içinden de delinebilir. Module kurallarını bir test project'inde, solution'daki bütün module'ler için tek seferde yazdık:

using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnitV3;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Bookstore.ArchitectureTests;

public sealed class ModuleBoundaryTests
{
    public static readonly TheoryData<string> Modules = ["Catalog", "Inventory", "Ordering", "Shipping", "Payments"];

    // Solution'daki bütün Bookstore assembly'leri, Host'un çıktı klasöründen
    private static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies([.. Directory.GetFiles(AppContext.BaseDirectory, "Bookstore.*.dll")
            .Where(f => !f.EndsWith("Tests.dll"))
            .Select(System.Reflection.Assembly.LoadFrom)])
        .Build();

    [Theory]
    [MemberData(nameof(Modules))]
    public void Core_does_not_depend_on_another_modules_core(string module) =>
        Types().That().ResideInNamespaceOrBelow($"Bookstore.{module}.Core").Should()
            .NotDependOnAny(Types().That().ResideInNamespaceMatching(@"^Bookstore\.\w+\.Core(\..*)?$")
                .And().DoNotResideInNamespaceOrBelow($"Bookstore.{module}.Core"))
            .Because("modules talk to each other only through Contracts")
            .Check(Architecture);

    [Theory]
    [MemberData(nameof(Modules))]
    public void Contracts_contain_only_interfaces_records_and_value_types(string module) =>
        Classes().That().ResideInNamespaceOrBelow($"Bookstore.{module}.Contracts").Should()
            .BeRecord().AndShould().BeSealed().AndShould().BeImmutable()
            .Because("a contract is a snapshot, not a model with behaviour")
            .WithoutRequiringPositiveResults()   // Contracts'ında henüz class olmayan module'ler için
            .Check(Architecture);

    [Theory]
    [MemberData(nameof(Modules))]
    public void Contracts_do_not_know_frameworks(string module) =>
        Types().That().ResideInNamespaceOrBelow($"Bookstore.{module}.Contracts").Should()
            .NotDependOnAny(Types(true).That().ResideInNamespaceOrBelow("Microsoft.EntityFrameworkCore")
                .Or().ResideInNamespaceOrBelow("Microsoft.AspNetCore"))
            .WithoutRequiringPositiveResults()
            .Check(Architecture);

    [Theory]
    [MemberData(nameof(Modules))]
    public void Only_the_module_entry_point_and_migrations_are_public_in_core(string module) =>
        Types().That().ResideInNamespaceOrBelow($"Bookstore.{module}.Core").And().ArePublic()
            .And().DoNotResideInNamespaceOrBelow($"Bookstore.{module}.Core.Persistence.Migrations").Should()
            .HaveFullName($"Bookstore.{module}.Core.{module}Module")
            .Because("everything else inside a module is internal")
            .Check(Architecture);
}

Dört kural, her biri beş module için çalışıyor, toplam 20 test. Birincisi, bir module'ün Core'unun başka bir module'ün Core'una bağımlı olmamasını type seviyesinde kontrol ediyor; ArchUnitNET derlenmiş IL'i (Intermediate Language) okuduğu için method gövdelerindeki kullanımları da görüyor. İlk yazıdaki reflection aracımızın kaçırdığı bağımlılıklar burada yakalanıyor. İkincisi, Contracts'taki class'ların sealed ve immutable record olmasını istiyor. Üçüncüsü, Contracts'ın EF (Entity Framework) Core ya da ASP.NET Core'a bağımlı olmamasını. Dördüncüsü, Core'da public olabilecek tek type'ın module'ün giriş noktası (CatalogModule, OrderingModule…) ve EF Core'un ürettiği migration class'ları olmasını.

İki ayrıntı. Test, assembly'leri tek tek typeof ile yüklemek yerine Host'un çıktı klasöründeki bütün Bookstore.*.dll dosyalarını yüklüyor; yeni bir module eklendiğinde kurallar onu kendiliğinden kapsıyor, yalnız Modules listesine adını yazmak gerekiyor. Ve Shipping ile Payments'ın Contracts'ında henüz class olmadığı için WithoutRequiringPositiveResults() kullandık. ArchUnitNET varsayılan olarak hiçbir type'a uymayan bir kuralı başarısız sayıyor; bu, namespace'i yanlış yazdığın için hiçbir şeyi kontrol etmeyen bir kuralın sessizce geçmesini engelleyen iyi bir varsayılan, ama boş olması beklenen yerlerde açıkça kapatılmalı.

Kurallar İlk Gün Ne Buldu?

Testleri ilk kez çalıştırdığımızda, hiçbir ihlal eklemeden bir test kırıldı:

failed Bookstore.ArchitectureTests.ModuleBoundaryTests.Only_the_module_entry_point_and_migrations_are_public_in_core(module: "Catalog") (0ms)
ArchUnitNET.xUnitV3.FailedArchRuleException : "Types that reside in namespace with full name "Bookstore.Catalog.Core" or below and are public and do not reside in namespace with full name "Bookstore.Catalog.Core.Persistence.Migrations" or below should have full name "Bookstore.Catalog.Core.CatalogModule" because everything else inside a module is internal" failed:
    Bookstore.Catalog.Core.DemoIds does have full name Bookstore.Catalog.Core.DemoIds

Dördüncü yazıda ChattyProbe aracının demo kitap id'lerini üretebilmesi için DemoIds'i public yapmıştık. Araç o yazıda kaldı, public anahtar kelimesi kaldı. Derleyici bunu bir sorun olarak görmüyor, code review'da kimse fark etmemişti ve Catalog'un dışarıya açık yüzeyi bir type genişlemişti. internal yaptık ve 20 testin hepsi geçti. Açılıştaki üçüncü pull request'in, yani kimsenin dokunmadığı ama zaten yanlış olan şeyin cevabı bu: onu yalnız otomatik bir kural yakalayabilir.

Kuralları Kırmak

İkinci pull request, Contracts'a eklenen mutable DTO (Data Transfer Object):

namespace Bookstore.Catalog.Contracts;

// "Ordering'in ekranı da bunu kullansın" diye eklenmiş, değiştirilebilir bir DTO
public class BookDto
{
    public Guid Id { get; set; }
    public string Title { get; set; } = "";
    public decimal Price { get; set; }
}

Build geçiyor, MSBuild kontrolü sessiz, çünkü project referanslarında bir değişiklik yok. Architecture test'i kırılıyor:

failed Bookstore.ArchitectureTests.ModuleBoundaryTests.Contracts_contain_only_interfaces_records_and_value_types(module: "Catalog") (1ms)
ArchUnitNET.xUnitV3.FailedArchRuleException : "Classes that reside in namespace with full name "Bookstore.Catalog.Contracts" or below should be record and should be sealed and should be immutable because a contract is a snapshot, not a model with behaviour" failed:
    Bookstore.Catalog.Contracts.BookDto is not record and is not sealed and is not immutable
  total: 20
  failed: 1
  succeeded: 19

Bir de MSBuild kontrolünün olmadığı bir dünyayı denedik: Directory.Build.targets'ı silip Ordering'e Catalog'un Core referansını ekledik ve Catalog'un public kalan tek type'ını, CatalogModule'ü bir method gövdesinden çağırdık:

using Bookstore.Catalog.Core;
using Microsoft.AspNetCore.Routing;

namespace Bookstore.Ordering.Core;

// Ordering'in testlerinde "Catalog endpoint'leri de lazım" diye eklenmiş bir kısayol
internal static class CatalogWarmup
{
    public static void MapEverything(IEndpointRouteBuilder app) => app.MapCatalogEndpoints();
}
failed Bookstore.ArchitectureTests.ModuleBoundaryTests.Core_does_not_depend_on_another_modules_core(module: "Ordering") (1ms)
ArchUnitNET.xUnitV3.FailedArchRuleException : "Types that reside in namespace with full name "Bookstore.Ordering.Core" or below should not depend on any Types that reside in namespace with full name matching "^Bookstore\.\w+\.Core(\..*)?$" and do not reside in namespace with full n... because modules talk to each other only through Contracts" failed:
    Bookstore.Ordering.Core.CatalogWarmup does depend on "Bookstore.Catalog.Core.CatalogModule"

İki bekçi birbirini tamamlıyor: MSBuild kontrolü referansı daha eklendiği anda durduruyor, ArchUnitNET kontrolü ise referans bir şekilde oraya girmişse kullanımın kendisini yakalıyor. Biri devre dışı kalırsa diğeri hâlâ orada.

NetArchTest mi, ArchUnitNET mi?

.NET'te bu iş için iki bilinen kütüphane var. Kamil Grzybek'in modular-monolith-with-ddd örneği architecture test'lerinde NetArchTest kullanıyor. Ancak NuGet'teki son sürümü, NetArchTest.Rules 1.3.2, Mayıs 2021 tarihli; o tarihten beri yeni sürüm çıkmamış. ArchUnitNET ise aktif olarak geliştiriliyor: kullandığımız 0.13.5, 26 Ekim 2026'da yayımlandı. Clean/Onion/Hexagonal serisinde de aynı sebeple ArchUnitNET'i seçmiştik. NetArchTest kullanan bir projen varsa çalışmaya devam eder, ama yeni bir başlangıç için bakım gören kütüphaneyi seçmek daha az risk.

Java dünyasındaki karşılaştırma da öğretici. Spring Modulith, module'leri package yapısından kendisi çıkarıp ApplicationModules.of(...).verify() çağrısıyla üç kuralı kontrol ediyor: module'ler arasında döngü olmaması, başka bir module'e yalnız API (Application Programming Interface) package'ı üzerinden erişilmesi ve isteğe bağlı olarak açıkça izin verilmiş bağımlılıklar. Bizim yazdığımız dört kural .NET'te aynı ihtiyacın elle kurulmuş hâli.

CODEOWNERS ile Module Sahipliği

Otomatik kurallar "ne yapılamaz"ı söyler, "kim karar verir"i söylemez. Bir module'ün Contracts'ında yapılan değişiklik, o module'ün tüketicilerini etkiler ve bunu module'ü yazan ekip tek başına değerlendirmemeli. GitHub'ın CODEOWNERS dosyası, bir path'e dokunan pull request'lerde hangi kişi ya da ekiplerin otomatik olarak inceleyici atanacağını tanımlıyor:

# Son eşleşen satır kazanır: genelden özele doğru yaz
*                                                  @bookstore/platform
/src/Modules/Catalog/                              @bookstore/catalog-search
/src/Modules/Ordering/                             @bookstore/ordering-payment
/src/Modules/Payments/                             @bookstore/ordering-payment
/src/Modules/Inventory/                            @bookstore/fulfillment
/src/Modules/Shipping/                             @bookstore/fulfillment
# Contracts tüketicileri de etkiler: module ekibi ve platform birlikte
/src/Modules/Catalog/Bookstore.Catalog.Contracts/  @bookstore/catalog-search @bookstore/platform
/src/Modules/Ordering/Bookstore.Ordering.Contracts/ @bookstore/ordering-payment @bookstore/platform
# Kuralların kendisi
/Directory.Build.targets                           @bookstore/platform
/tests/Bookstore.ArchitectureTests/                @bookstore/platform
/.github/CODEOWNERS                                @bookstore/platform

GitHub dokümanındaki iki kural bu dosyanın şeklini belirliyor. Birincisi, bir dosyaya uyan satırlardan sonuncusu geçerli; bu yüzden genelden özele doğru yazdık. İkincisi, aynı path için ayrı satırlarda yazılan sahipler birleşmiyor, yalnız son satır sayılıyor; Contracts satırlarında module ekibini ve platform ekibini aynı satıra bu yüzden koyduk. Dosyanın kendisinin, Directory.Build.targets'ın ve architecture test'lerinin sahibi de platform ekibi: kuralları gevşetmek bir module ekibinin tek başına verebileceği bir karar olmamalı. Bu dosyanın bir etkisi olması için branch protection ya da ruleset'te "Require review from Code Owners" ayarının açık olması gerekiyor. Ekip isimleri, Bookstore'un 12–18 ay içinde kuracağı dört stream-aligned ekiple aynı; Mimariye Giriş serisinin "Conway's Law: Ekip Yapısı Mimariyi Nasıl Şekillendirir?" yazısında ekip ve module sınırlarının neden örtüşmesi gerektiğini konuşmuştuk.

Module'ü Tek Başına Test Etmek

Architecture test'leri yapıyı korur, davranışı değil. Bir module'ün davranışını test etmenin modular monolith'e özgü bir avantajı var: module, komşularının yalnız contract'larını tanıdığı için onu tek başına ayağa kaldırabilirsin. ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Integration Test: WebApplicationFactory ile Uçtan Uca Test" yazısında WebApplicationFactory ile bütün uygulamayı test etmiştik. Burada aynı altyapıyı, TestServer'ı, yalnız Ordering için kullanıyoruz:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="4.0.2" />
    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.14" />
  </ItemGroup>
  <ItemGroup>
    <!-- Yalnız Ordering ve diğer module'lerin Contracts'ı: Catalog.Core ya da Inventory.Core yok -->
    <ProjectReference Include="..\..\src\Modules\Ordering\Bookstore.Ordering.Core\Bookstore.Ordering.Core.csproj" />
  </ItemGroup>
</Project>
public sealed class OrderingModuleHost : IAsyncLifetime
{
    private readonly string _connectionString =
        $"{Environment.GetEnvironmentVariable("ORDERING_TEST_SQL")};Database=ordering_tests_{Guid.NewGuid():N}";
    private WebApplication _app = null!;

    public FakeCatalog Catalog { get; } = new();
    public FakeInventory Inventory { get; } = new();
    public HttpClient Client { get; private set; } = null!;

    public async ValueTask InitializeAsync()
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
        builder.Logging.ClearProviders();
        builder.Services
            .AddInProcessMessaging()
            .AddOrderingModule(_connectionString, useOutbox: true)
            .AddSingleton<ICatalogModuleApi>(Catalog)
            .AddSingleton<IInventoryModuleApi>(Inventory);

        _app = builder.Build();
        await _app.Services.InitializeOrderingModuleAsync();
        _app.MapOrderingEndpoints();
        await _app.StartAsync();
        Client = _app.GetTestClient();
    }

    public async Task<int> OutboxCountAsync(Guid orderId)
    {
        using IServiceScope scope = _app.Services.CreateScope();
        OrderingDbContext db = scope.ServiceProvider.GetRequiredService<OrderingDbContext>();
        return await db.OutboxMessages.CountAsync(m => m.Payload.Contains(orderId.ToString()));
    }

    public async ValueTask DisposeAsync()
    {
        using (IServiceScope scope = _app.Services.CreateScope())
            await scope.ServiceProvider.GetRequiredService<OrderingDbContext>().Database.EnsureDeletedAsync();
        await _app.DisposeAsync();
    }
}

Test host'u yalnız AddOrderingModule()'ü çağırıyor; Catalog ve Inventory, contract'larının sahte implementation'larıyla temsil ediliyor. Ordering'in kendi veritabanı gerçek: her test sınıfı için SQL (Structured Query Language) Server'da adı rastgele bir veritabanı açılıyor, Ordering'in migration'ları uygulanıyor ve sınıf bitince veritabanı siliniyor. Diğer module'lerin schema'ları bu veritabanında hiç yok ve test yine de geçiyor; Ordering'in başka bir module'ün tablosuna gizlice bağımlı olmadığının en somut kanıtı bu.

using System.Net;
using System.Net.Http.Json;
using Xunit;

namespace Bookstore.Ordering.Tests;

public sealed class PlaceOrderTests(OrderingModuleHost host) : IClassFixture<OrderingModuleHost>
{
    private sealed record Confirmation(Guid OrderId, int Lines, decimal Total, string Currency);

    [Fact]
    public async Task Order_is_saved_with_its_outbox_message()
    {
        host.Inventory.Available = 5;
        HttpResponseMessage response = await host.Client.PostAsJsonAsync("/api/ordering/orders",
            new { lines = new[] { new { bookId = FakeCatalog.Dune.Value, quantity = 2 } } }, TestContext.Current.CancellationToken);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Confirmation? confirmation = await response.Content.ReadFromJsonAsync<Confirmation>(TestContext.Current.CancellationToken);
        Assert.Equal(25.00m, confirmation!.Total);
        Assert.Equal(1, await host.OutboxCountAsync(confirmation.OrderId));
    }

    [Fact]
    public async Task Order_is_rejected_when_inventory_has_no_stock()
    {
        host.Inventory.Available = 0;
        HttpResponseMessage response = await host.Client.PostAsJsonAsync("/api/ordering/orders",
            new { lines = new[] { new { bookId = FakeCatalog.Dune.Value, quantity = 1 } } }, TestContext.Current.CancellationToken);

        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
    }
}

Solution'daki bütün test'lerin çıktısı:

tests/Bookstore.ArchitectureTests Bookstore.ArchitectureTests.dll (net10.0|arm64) passed (725ms)
tests/Bookstore.Ordering.Tests Bookstore.Ordering.Tests.dll (net10.0|arm64) passed (1s 618ms)
Test run summary: Passed!
  total: 22
  failed: 0
  succeeded: 22

Bu test'in sınırı da belli: Ordering'in, Inventory'nin contract'ını doğru yorumladığını test ediyor, Inventory'nin o contract'a uyduğunu değil. İkincisi Inventory'nin kendi test'lerinin işi. İki module arasındaki uyumu tek bir yerde görmek istersen, bütün Host'u WebApplicationFactory ile kaldıran az sayıda uçtan uca test ekle.

Sık Düşülen Tuzaklar

Kuralları tek bir module için yazıp kopyalamak

Ordering_does_not_depend_on_Catalog_core, Ordering_does_not_depend_on_Inventory_core… Module sayısı arttıkça kurallar karesel büyür ve yeni module'ü eklemeyi unutan biri onu korumasız bırakır. Kuralları module listesi üzerinden parametrik yaz.

Boş sonuçlu kuralları fark etmemek

Namespace'i yanlış yazılmış bir kural hiçbir type'ı kontrol etmez. ArchUnitNET'in varsayılanı bunu bir hata sayıyor; WithoutRequiringPositiveResults()'ı yalnız gerçekten boş olması beklenen yerlerde kullan.

Architecture test'lerini Release'te koşturmak

ArchUnitNET'in README'si test'lerin Debug konfigürasyonunda çalıştırılmasını öneriyor, çünkü Release derlemesindeki optimizasyonlar bazı bağımlılıkları IL'den silebilir. CI'da dotnet test'i varsayılan Debug konfigürasyonuyla bırak.

İhlalleri "geçici" olarak kural dışı bırakmak

Kırılan bir kuralı susturmak için listeye bir istisna eklemek, Shopify'ın deneyiminde gördüğümüz gibi kuralın anlamını zamanla yok eder. İstisna gerekiyorsa sahibi ve bitiş tarihi olan, CODEOWNERS'ın platform ekibine gönderdiği bir değişiklik olmalı.

Module test'inde gerçek komşuları kullanmak

Ordering'in test host'una "nasılsa çalışıyor" diye Catalog'un gerçek module'ünü eklemek, Ordering'in test'ini Catalog'un veritabanına ve verisine bağlar. Module test'i komşularını contract seviyesinde fake'lemeli.

Küçük Bir Deneme

★☆☆ Dördüncü bir kural ekle: Contracts project'lerindeki interface'lerin adı ModuleApi ile bitmeli. Mevcut kod bu kurala uyuyor mu?

★★☆ Directory.Build.targets'taki kontrolü genişlet: Contracts project'leri yalnız Bookstore.SharedKernel, Bookstore.BuildingBlocks ve başka Contracts project'lerine referans verebilsin. Kasıtlı bir ihlalle hata mesajını gör.

★★★ Inventory için OrderingModuleHost benzeri bir test host'u yaz. ReserveStockWhenOrderPlaced handler'ını, gerçek bir OrderPlaced event'ini bus'a vererek test et ve aynı event'i iki kez verdiğinde rezervasyonun bir kez yapıldığını doğrula.

Bu yazıda module sınırlarını otomatik bekçilere emanet ettik. Serinin son yazısında bu sınırların bize kazandırdığı en büyük seçeneği kullanıyoruz: gerçekten gerektiğinde bir module'ü ayrı bir service'e çıkarmak. Ne zaman, hangi sırayla ve her adımda geri dönebilecek şekilde.