Mert Özen Her Satırda Daha İleri
Backend .NET ile Mikroservis Uygulaması — #13

Microservice'leri Test Etmek: Service Testleri, Aspire ile Uçtan Uca Test ve Contract Testleri

Mert Özen 14 Kas 2026 13 dk 7 görüntülenme
Microservice'leri Test Etmek: Service Testleri, Aspire ile Uçtan Uca Test ve Contract Testleri

Microservice'lerde testin ağırlığı service sınırında: Ordering, Testcontainers ile gerçek SQL Server ve RabbitMQ'ya karşı test ediliyor ve 07'deki saga çakışması 10 denemede 9 kez yaşanıp retry ile tutarlı kalıyor. Event şemaları JSON Schema snapshot'ıyla, BFF–Catalog HTTP contract'ı Pact ile doğrulanıyor; Aspire.Hosting.Testing ile tek bir checkout bütün sistemde push beklenerek test ediliyor.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Aspire 13.6 (Aspire.Hosting.Testing 13.6.5), Testcontainers for .NET 4.16, PactNet 5.0, Microsoft.AspNetCore.Mvc.Testing 10.0 ve xUnit v3 ile derlenip denenmiştir.

Önce Tahmin Et

07'de saga için bir iddiada bulunmuştuk: ödeme ile 15 dakikalık timeout aynı anda gelirse saga'nın Version kolonu ikinci kaydı reddeder ve Wolverine mesajı yeniden dener. Bunu hiç gerçekten denemedik. Aynı siparişe PaymentSucceeded ve CheckoutTimedOut'u on kez aynı anda gönderirsek kaç çakışma olur ve kaç sipariş tutarsız kalır? İkinci soru: Catalog ekibi kitap cevabındaki author alanını authorName yapıyor. Catalog'un kendi testleri geçer. Bu değişikliği hangi test, hangi pipeline'da yakalamalı? Cevaplar bu yazıda yazdığımız testlerin gerçek çıktılarında.

Microservice'te Test Piramidi

Şimdiye kadar sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) davranışlarını çoğunlukla elle denedik: tarayıcıda sipariş verdik, service durdurduk, log okuduk. Elimizde 33 birim test vardı; saga, domain model, webhook imzası ve metric'ler milisaniyeler içinde test ediliyordu. Ama bu testler mesajın gerçekten RabbitMQ'ya gittiğini, iki service'in aynı JSON'u konuştuğunu ya da bütün sistemin birlikte çalıştığını göstermiyor. Test Stratejisi serisinin "Test Pyramid, Testing Trophy ve Honeycomb" yazısında microservice'ler için honeycomb şeklinin neden öne çıktığını anlattık: en kalın katman service'in kendi sınırındaki testler. Bookstore'da katmanlar şöyle oldu:

KatmanProjeNeyi gösteriyorSüre
BirimBookstore.Ordering.Tests ve diğerleriSaga adımları, domain kuralları, metric'ler0,2–0,3 sn (33 test)
ServiceBookstore.Ordering.IntegrationTestsGerçek SQL Server ve RabbitMQ ile Ordering15 sn (tek başına)
ContractBookstore.Contracts.Tests, Pact projeleriEvent şemaları, BFF (Backend for Frontend; Angular'a özel ara backend) ile Catalog arasındaki HTTP contract'ışema 0,2 sn; Pact tüketici 0,3 sn, sağlayıcı 15–26 sn
Uçtan ucaBookstore.EndToEndTestsBütün sistemle tek bir checkout80 sn

Süreler bu makinede, Docker image'ları önceden indirilmişken ölçüldü.

Service Testi: WebApplicationFactory ve Testcontainers

Service testi, Ordering'i gerçek bir process sınırı olarak ele alıyor: kendi SQL Server'ı ve RabbitMQ'su container'da, Program.cs olduğu gibi, yalnız authentication değiştirilmiş. WebApplicationFactory ve Testcontainers'ın ayrıntıları Integration ve E2E (End-to-End; uçtan uca) Test serisinin ikinci ve üçüncü yazılarında; burada microservice'e özgü kısımlara bakıyoruz:

public async ValueTask InitializeAsync()
{
    await Task.WhenAll(sql.StartAsync(), rabbitmq.StartAsync());
    string orderingDb = new SqlConnectionStringBuilder(sql.GetConnectionString()) { InitialCatalog = "orderingdb" }.ConnectionString;

    // Önce şema (Aspire'da migrator'ın işi): Wolverine açılışta kendi tablolarını bu veritabanında oluşturuyor.
    // Sonra Ordering'in ihtiyaç duyduğu veri: iki kitabın kopyası.
    await using (OrderingDbContext db = new(new DbContextOptionsBuilder<OrderingDbContext>().UseSqlServer(orderingDb).Options))
    {
        await db.Database.MigrateAsync();
        db.CatalogBooks.AddRange(
            new CatalogBook { Isbn = "9780141439587", Title = "Emma", Price = 249.90m, Currency = "TRY", PriceVersion = 1, TitleVersion = 1 },
            new CatalogBook { Isbn = "9780441172719", Title = "Dune", Price = 489m, Currency = "TRY", PriceVersion = 1, TitleVersion = 1 });
        await db.SaveChangesAsync();
    }

    Factory = new WebApplicationFactory<Program>().WithWebHostBuilder(web =>
    {
        // Aspire'ın WithReference ile service'e verdiği adların aynısı
        web.UseSetting("ConnectionStrings:orderingdb", orderingDb);
        web.UseSetting("ConnectionStrings:rabbitmq", RabbitMqUri);
        web.ConfigureLogging(logging => logging.AddProvider(Logs));
        web.ConfigureTestServices(services =>
        {
            // Bu testte Keycloak yok: her istek Ada, bir müşteri
            services.AddAuthentication(TestUser.SchemeName)
                .AddScheme<AuthenticationSchemeOptions, TestUser>(TestUser.SchemeName, _ => { });
        });
    });
}

İlk denemede migration'ı factory'nin service'leri üzerinden çalıştırdık ve bütün testler aynı hatayla düştü: Error Number:4060, yani orderingdb açılamadı. Factory'nin Services'ine dokunmak host'u başlatıyor; Wolverine de açılışta kendi inbox/outbox tablolarını oluşturmaya çalışıyor, veritabanı ise henüz yok. Aspire'da bu sırayı WaitForCompletion(migrator) sağlıyordu (02); testte aynı sırayı elle kurmak gerekiyor. Program.cs'e test için bir şey eklemedik: .NET 10'da top-level statement'lardan üretilen Program class'ı testlerin göreceği şekilde public.

Test, mesajları RabbitMQ'dan kendisi alıyor. Ordering'in exchange'ine bir queue bağlıyor (Notifications rolünde) ve inventory.commands queue'sunu kendisi tanımlıyor (Inventory rolünde). 07'de söylediğimiz gibi Ordering bu queue'yu ExternallyOwned() ile tanımlamıyor; test tanımlamasaydı ReserveStock sessizce kaybolurdu. Mesaj geldiğinde consumer bir channel'a yazıyor ve test onu bekliyor; döngüyle polling yok, 15 saniyelik bir üst sınır var:

[Fact]
public async Task An_accepted_order_publishes_OrderPlaced_and_asks_Inventory_for_stock()
{
    HttpClient client = ordering.Factory.CreateClient();
    // Test Notifications'ın (Ordering'in exchange'ine bağlı bir queue) ve Inventory'nin (command queue'su) yerinde
    await using QueueProbe events = await QueueProbe.StartAsync(ordering.RabbitMqUri, "test.ordering-events", "bookstore.ordering");
    await using QueueProbe inventory = await QueueProbe.StartAsync(ordering.RabbitMqUri, "inventory.commands");

    HttpResponseMessage response = await client.PostAsJsonAsync("/api/v1/orders",
        new { items = new[] { new { isbn = "9780141439587", quantity = 2, unitPrice = 249.90m } } }, Ct);

    Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
    ReceivedMessage placed = await events.NextAsync("Bookstore.Contracts.Ordering.OrderPlaced", Wait);
    Assert.Contains($"\"customerId\":\"{TestUser.Subject}\"", placed.Body);
    Assert.Contains("\"total\":499.80", placed.Body);
    // Trace context mesajla birlikte taşınıyor (12. yazı)
    Assert.StartsWith("00-", placed.Headers["parent-id"]);
    ReceivedMessage reserve = await inventory.NextAsync("Bookstore.Contracts.Inventory.ReserveStock", Wait);
    Assert.Contains("\"quantity\":2", reserve.Body);
}

Bu test birim testlerin gösteremediği üç şeyi birlikte doğruluyor: outbox'ın commit'ten sonra gerçekten boşaldığını, exchange ve queue adlarının doğru olduğunu ve JSON'un consumer'ların beklediği biçimde (camelCase, customerId token'dan) çıktığını. Container'ların açılması dahil test sınıfı yaklaşık 15 saniye sürdü. Testcontainers'ın varsayılan olarak başlattığı temizlik container'ını (Ryuk) TESTCONTAINERS_RYUK_DISABLED=true ile kapattık ve container'ları DisposeAsync'te kendimiz siliyoruz; CI'da Ryuk'u açık bırakmak, yarıda kesilen bir koşunun container'larını da temizlediği için daha güvenli.

Saga Yarışını Gerçekten Denemek

Önce Tahmin Et'in ilk sorusu için aynı fixture'ı kullanan ikinci bir test yazdık. Sipariş veriliyor, test Inventory rolünde StockReserved'ı gönderiyor ve saga ödeme beklemeye geçiyor. Sonra ödeme ve timeout aynı anda, ayrı IMessageBus scope'larından gönderiliyor:

// Aynı saga için ödeme ve timeout, aynı anda, her biri kendi bus'ıyla
await Task.WhenAll(
    Bus().InvokeAsync(new PaymentSucceeded(orderId, $"pay_race_{i}"), Ct),
    Bus().InvokeAsync(new CheckoutTimedOut(orderId, TimeSpan.Zero), Ct));

await using AsyncServiceScope scope = ordering.Factory.Services.CreateAsyncScope();
OrderingDbContext db = scope.ServiceProvider.GetRequiredService<OrderingDbContext>();
Order order = await db.Orders.AsNoTracking().SingleAsync(o => o.Id == new OrderId(orderId), Ct);
CheckoutSaga? saga = await db.Set<CheckoutSaga>().AsNoTracking().SingleOrDefaultAsync(s => s.Id == orderId, Ct);

// Yalnız iki tutarlı son var: ödendi ve stok kesinleşmesini bekliyor, ya da iptal edildi ve iade yolda
if (order.Status == OrderStatus.Paid)
{
    Assert.Equal(CheckoutStep.AwaitingStockCommit, saga?.Step);
    outcomes.Add("payment first");
}
else
{
    Assert.Equal(OrderStatus.Cancelled, order.Status);
    Assert.Null(saga);
    await payments.NextAsync("Bookstore.Contracts.Payments.RefundPayment", Wait);
    outcomes.Add("timeout first");
}

Test bu bloğu on kez çalıştırıyor ve Ordering'in log'larından çakışmaları sayıyor. Sonuç:

passed Bookstore.Ordering.IntegrationTests.PlaceOrderTests.A_payment_racing_the_timeout_leaves_the_order_consistent (1s 675ms)
    payment first: 10; saga version conflicts: 9

Error: Invocation of CheckoutTimedOut { DelayTime = 00:00:00, OrderId = 01a1dad9-f5f0-757e-8b76-179cc9d2e0f6, After = 00:00:00 } failed! SagaConcurrencyException

On denemenin dokuzunda iki mesaj saga'yı aynı anda yükledi ve timeout'un kaydı Version kontrolüne takıldı: Wolverine SagaConcurrencyException'ı log'ladı, 07'deki retry kuralı mesajı yeniden çalıştırdı, timeout handler'ı bu kez saga'yı AwaitingStockCommit'te buldu ve hiçbir şey yapmadı. Tutarsız kalan sipariş sıfır. Hep ödemenin kazanması bu ölçümün özelliği, kuralın değil: iki InvokeAsync'in hangisinin önce kaydettiği sıralamaya bağlı ve test iki sonucu da kabul ediyor. 07'deki iddia artık bir test; bu davranışı bozan bir değişiklik CI'da kırmızıya düşer.

Event Contract'ları: Şema Testi

Mesaj contract'ları Bookstore.Contracts'taki record'lar ve 06'daki kural belli: alan eklemek serbest, alan silmek ya da adını değiştirmek yeni bir mesaj tipi demek. Kuralı insanların dikkatine bırakmamak için her mesajın JSON şemasını System.Text.Json'ın JsonSchemaExporter'ı ile üretip repository'deki onaylı şemayla karşılaştırıyoruz. Message Queue'larla Çalışmak serisinin test yazısındaki contract testlerinin Bookstore'daki karşılığı bu:

Type type = typeof(Exchanges).Assembly.GetType(typeName)!;
string actual = JsonSchemaExporter.GetJsonSchemaAsNode(Wire, type).ToJsonString(new JsonSerializerOptions { WriteIndented = true });
string approvedPath = Path.Combine(SourceFolder(), "schemas", $"{typeName}.json");
...
JsonNode approved = JsonNode.Parse(File.ReadAllText(approvedPath))!;
JsonNode current = JsonNode.Parse(actual)!;
string[] removed = [.. Properties(approved).Except(Properties(current))];
string[] added = [.. Properties(current).Except(Properties(approved))];

// Bir alanı silmek ya da adını değiştirmek (ya da tipini değiştirmek) deploy edilmiş consumer'ları bozar
Assert.True(removed.Length == 0, $"{typeName}: breaking change, removed or changed: {string.Join(", ", removed)}");
// Alan eklemek versioning kuralımıza göre serbest (06. yazı), ama onaylı şema bilerek güncellenmeli
Assert.True(added.Length == 0, $"{typeName}: new fields {string.Join(", ", added)}; update schemas/{typeName}.json");

Test, assembly'deki 17 record'u kendisi buluyor; yeni bir mesaj eklendiğinde şeması yoksa bir .received dosyası yazıp düşüyor ve dosyanın adını değiştirmek onay oluyor. Denemek için OrderPlaced'ın CustomerEmail alanını Email yaptık ve bir CouponCode ekledik:

failed Bookstore.Contracts.Tests.MessageSchemaTests.Wire_format_matches_the_approved_schema(typeName: "Bookstore.Contracts.Ordering.OrderPlaced") (0ms)
  Bookstore.Contracts.Ordering.OrderPlaced: breaking change, removed or changed: customerEmail:"string"
  total: 17
  failed: 1

Breaking change kendi adıyla yakalandı ve diğer 16 mesaj etkilenmedi. Şema dosyaları pull request'te diff olarak göründüğü için, yeni bir alanın onaylanması da bir code review kararı oluyor.

Pact: BFF ile Catalog Arasındaki Contract

HTTP tarafında 03'ten beri oasdiff ile Catalog'un OpenAPI dokümanındaki breaking change'leri yakalıyoruz. Ama oasdiff Catalog'un kendi dokümanına bakıyor; BFF'nin bu dokümandan neyi gerçekten kullandığını bilmiyor. Pact tersinden çalışıyor: tüketici (consumer) ihtiyacını bir test olarak yazıyor, sağlayıcı (provider) bu beklentiyi kendi gerçek koduyla doğruluyor. BFF tarafında test, BFF'nin gerçek CatalogApi'sini Pact'in mock server'ına karşı çalıştırıyor:

pact.UponReceiving("a request for an existing book")
    .Given("the book 9780141439587 exists")
    .WithRequest(HttpMethod.Get, "/api/v1/books/9780141439587")
    .WillRespond()
    .WithStatus(HttpStatusCode.OK)
    // Yalnız BFF'nin okuduğu alanlar, birebir değer yerine tipleriyle: Catalog istediği alanı ekleyebilir
    .WithJsonBody(new
    {
        isbn = "9780141439587",
        title = Match.Type("Emma"),
        author = Match.Type("Jane Austen"),
        category = Match.Type("Classics"),
        price = Match.Decimal(249.90m),
        currency = Match.Regex("TRY", "^[A-Z]{3}$"),
        publishedYear = Match.Integer(1815),
        description = Match.Type("A young woman in a small English village keeps arranging other people's marriages.")
    });

await pact.VerifyAsync(async context =>
{
    CatalogApi catalog = new(new HttpClient { BaseAddress = context.MockServerUri });

    CatalogBook? book = await catalog.GetBookAsync("9780141439587", TestContext.Current.CancellationToken);

    Assert.Equal("Emma", book?.Title);
    Assert.Equal(249.90m, book?.Price);
});

İkinci etkileşim bilinmeyen bir kitap için 404 bekliyor. İki test 0,5 saniyede geçti ve tests/pacts/Bookstore.Bff-Bookstore.Catalog.json dosyasını üretti. Catalog tarafında gerçek Catalog, WebApplicationFactory ile ama gerçek bir port'ta çalışıyor; .NET 10'daki UseKestrel bunu sağlıyor ve Pact'in doğrulayıcısı ayrı bir native süreç olduğu için bellekteki TestServer'ı kullanamıyor. SQL Server ve Redis Testcontainers'dan, RabbitMQ yerine Wolverine'in DisableAllExternalWolverineTransports()'ı var; event'ler bu contract'ın konusu değil. Pact her etkileşimden önce Given metnini /provider-states'e gönderiyor ve test o durumu hazırlıyor:

Verifying a pact between Bookstore.Bff and Bookstore.Catalog
  a request for a book that does not exist (0s loading, 402ms verification)
     Given the book 9780000000000 does not exist
    returns a response which
      has status code 404 (OK)
      has a matching body (OK)
  a request for an existing book (0s loading, 309ms verification)
     Given the book 9780141439587 exists
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (OK)

Önce Tahmin Et'in ikinci sorusu: Catalog'da Author property'sine [JsonPropertyName("authorName")] ekledik. Catalog'un kendi testleri bunu fark etmez; provider doğrulaması etti:

1) Verifying a pact between Bookstore.Bff and Bookstore.Catalog Given the book 9780141439587 exists - a request for an existing book
    1.1) has a matching body
           $ -> Actual map is missing the following keys: author
    {
    -  "author": "Jane Austen",
    +  "authorName": "Jane Austen",
      "category": "Classics",

Cevabın Catalog'un döndürdüğü ama BFF'nin okumadığı version alanı hata değil; tolerant reader'ın (yalnız ihtiyaç duyduğu alanları okuyan, gerisini yok sayan istemci) contract'taki karşılığı bu. Doğru yer de önemli: bu test Catalog'un pipeline'ında koşmalı, çünkü kırılan Catalog'un değişikliği. 14'te bunu GitHub Actions'ta kuracağız. Pact'in native library'si varsayılan olarak anonim kullanım istatistiği gönderiyor; her iki test projesinde PACT_DO_NOT_TRACK=true ile kapattık.

Aspire ile Uçtan Uca: Tek Bir Checkout

En üst katmanda tek bir test var ve bütün sistemi AppHost'tan kuruyor: SQL Server, RabbitMQ, Redis, Keycloak, Mailpit ve bütün service'ler. Test Angular'ın yaptığını yapıyor: BFF üzerinden giriş yapıyor, hub'a bağlanıyor ve sipariş veriyor. Saga'nın bitişini push'tan öğreniyor; sabit bir bekleme ya da durum sorgulayan bir döngü yok:

// Angular'ın açtığı hub bağlantısının aynısı, aynı oturum cookie'siyle
TaskCompletionSource<OrderStatusChanged> confirmed = new(TaskCreationOptions.RunContinuationsAsynchronously);
Guid orderId = Guid.Empty;
await using HubConnection hub = new HubConnectionBuilder()
    .WithUrl(new Uri(bff, "/hubs/orders"), options => options.Cookies = cookies)
    .Build();
hub.On<OrderStatusChanged>("OrderStatusChanged", change =>
{
    if (change.OrderId == orderId && change.Status is "Confirmed" or "Cancelled")
    {
        confirmed.TrySetResult(change);
    }
});
await hub.StartAsync(Ct);
...
// Saga'nın sonu bir push olduğu için test push'u bekliyor: polling döngüsü yok, sabit bekleme yok
OrderStatusChanged result = await confirmed.Task.WaitAsync(TimeSpan.FromSeconds(60), Ct);
Assert.Equal("Confirmed", result.Status);

Bu testi yeşile çevirmek, testin kendisini yazmaktan uzun sürdü. Dört engel çıktı ve dördü de microservice'leri uçtan uca test eden herkesin karşısına çıkabilir:

  • Lokal ortamla çakışma. AppHost'taki SQL Server, RabbitMQ ve Keycloak persistent ve adlandırılmış volume'lu. Test onları yeniden kullanınca bir önceki koşunun verisiyle çalıştı. Testte bu container'ları session ömrüne çevirdik, volume'larını kaldırdık ve adlarını msapp-e2e- ile değiştirdik; her koşu boş veritabanlarıyla başlıyor ve test bitince container'lar siliniyor.
  • HTTPS profili. Testing builder AppHost'un ilk launch profile'ını (https) seçti ve Catalog, Payments ve Notifications açılışta Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found ile kapandı. CI agent'larında da genellikle güvenilen bir geliştirici sertifikası olmaz; DOTNET_LAUNCH_PROFILE=http ile lokalde kullandığımız profili seçtik.
  • Rastgele port'lar. Testing builder varsayılan olarak her endpoint'e rastgele bir port veriyor; Keycloak localhost:63682'de açıldı. 10'daki realm ise yalnız http://localhost:5100/signin-oidc'ye yönlendirmeye izin veriyor. DcpPublisher:RandomizePorts=false ile BFF ve Keycloak sabit port'larında kaldı.
  • Secure cookie'ler. Tarayıcılar http://localhost'u güvenli bir context sayıyor ve Secure işaretli cookie'leri ona gönderiyor; HttpClient göndermiyor. ASP.NET Core'un OIDC (OpenID Connect; OAuth 2.0 üstündeki kimlik katmanı) correlation cookie'leri ve Keycloak'un oturum cookie'leri Secure, bu yüzden giriş /bff/user'da 401 ile bitti. Test, yönlendirmeleri kendisi takip eden ve her adımda bu işareti kaldıran küçük bir DelegatingHandler kullanıyor.
passed Bookstore.EndToEndTests.CheckoutEndToEndTests.A_placed_order_is_confirmed_and_the_browser_is_told (1m 20s 009ms)
    system healthy after 61.2 s
    order 01a1db0b-b481-7a0b-83c2-8b6348b46e9e Confirmed 4.35 s after POST

Sistemin ayağa kalkması yaklaşık bir dakika, siparişin onaylanması dört-beş saniye sürdü; ikincisinin büyük kısmı Wolverine'in ilk mesajlarda handler kodunu derlemesi (07). Bu testin değeri her şeyin birlikte çalıştığını göstermesi; bir hatanın yerini göstermesi değil. O yüzden tek bir happy path test ediyoruz. Ödeme reddi, stok yokluğu ve zaman aşımı saga'nın birim testlerinde ve service testinde.

Üretimde Dikkat

CI'ın süresi ve maliyeti test katmanlarıyla birlikte büyüyor. Bütün solution'ın 56 testi bu makinede 82,7 saniye sürdü. Test projeleri paralel koştuğu için toplam süreyi neredeyse tek başına uçtan uca test belirliyor; service testi ve Pact sağlayıcısı aynı anda koşarken 26–29 saniyeye çıktı. Her pull request'te bütün katmanları koşmak yerine birim, service ve contract testlerini her değişiklikte, uçtan uca testi main'e merge'den sonra ya da gece koşmak bir seçenek.

CI agent'ında Docker gerekiyor. Testcontainers ve Aspire testleri container başlatıyor; GitHub'ın Linux runner'larında Docker var, Windows ve macOS runner'larında Linux container çalıştırmak ya mümkün değil ya da ek kurulum istiyor. Image'ları önbelleğe almak (ya da kendi registry'nizden çekmek) ilk koşunun dakikalarını kısaltır.

Pact dosyaları için bir Pact Broker kullanın. Biz pact dosyasını repository'de tutuyoruz, çünkü iki taraf aynı repository'de. Ayrı repository'lerdeki ekipler pact'leri bir broker'a yayınlar ve provider pipeline'ı oradan çeker; broker hangi tüketici sürümünün hangi sağlayıcı sürümüyle doğrulandığını da tutar ve deploy kararı buna göre verilebilir.

Uçtan uca testi kararsız yapan şeyler: sabit beklemeler, paylaşılan veri ve rastgele port. Biz üçünü de kaldırdık: bekleme sinyalle, veri her koşuda boş veritabanıyla, port'lar sabit. Yine de bu test, bir container'ın yavaş açılması gibi altyapı nedenleriyle diğerlerinden daha sık kırılacak; kırıldığında resource log'larına bakın (Aspire testing onları test çıktısına Bookstore.AppHost.Resources.<ad> kategorisiyle yazıyor).

Sık Düşülen Tuzaklar

Her şeyi uçtan uca test etmek. Ödeme reddini, stok yokluğunu ve timeout'u bütün sistemle denemek her biri için bir dakika ve kararsız bir test demek; aynı davranış saga'nın birim testinde milisaniyelerde.

Asenkron sonucu Task.Delay ile beklemek. Ya gereksiz uzun bekler ya da yavaş bir günde kırılır. Bekleyeceğin şey bir mesaj ya da push ise onu dinle.

Testte servisin dışarıya ait queue'larını unutmak. ExternallyOwned bir queue test ortamında yoksa mesaj hatasız kaybolur ve test "mesaj gönderildi"yi yanlışlıkla doğrular.

Consumer-driven contract'ı provider'ın pipeline'ına koymamak. Pact doğrulaması yalnız BFF'nin pipeline'ında koşarsa Catalog'un kırıcı değişikliği Catalog deploy edildikten sonra görülür.

Pact'e bütün cevabı yazmak. Tüketicinin okumadığı alanları da contract'a koymak, sağlayıcının zararsız değişikliklerini de kırmızıya çevirir.

Küçük Bir Deneme

★☆☆ Şema testinde OrderPlacedLine'ın Quantity alanını int'ten long'a çevir. Test ne diyor? JSON'da gerçekten bir değişiklik var mı, ve bu değişiklik bir consumer'ı bozar mı?

★★☆ Ordering'in service testine Catalog'un BookPriceChanged event'ini ekle: testi Catalog rolünde bookstore.catalog exchange'ine bir mesaj yayınlat ve kopyanın fiyatının değiştiğini, aynı mesaj ikinci kez gelince değişmediğini doğrula.

★★★ Uçtan uca teste ikinci bir senaryo ekle: Payments'ı test içinde durdur (app.ResourceCommands), sipariş ver, push ile Pending'i gör, Payments'ı başlat ve onayı bekle. Bu test ne kadar sürüyor ve hangi kısmı birim testlerinde zaten var?

Testler hazır, şimdi bu sistemi bir yere koymak gerekiyor. Bir sonraki yazıda image'ları .NET SDK'sı ile üretiyor, Angular'ı BFF'nin image'ına gömüyor, Aspire'ın Kubernetes çıktısını elle yazılmış manifest'lerle karşılaştırıyor ve her service için yalnız kendi değişikliğinde çalışan bir GitHub Actions pipeline'ı kuruyoruz.