Eski Integration ve E2E Testlerini Okumak: WebApplicationFactory .NET 6'dan 11'e, Testcontainers 3'ten 4'e, Protractor'dan Playwright'a
Eski bir integration testini okurken ilk soru, factory'nin hangi uygulamayı ayağa kaldırdığıdır: Startup tabanlı bir factory .NET 10'da derlenip Bookstore'u hiç çalıştırmadan 404 döndürebilir. WebApplicationFactory'nin .NET 6'dan 11'e değişimini, .NET 11'in ConfigureWebApplicationBuilder hook'unu, Testcontainers 4 değişikliklerini, Protractor'dan geçişi ve Playwright 1.58–1.65'teki kırıcı değişiklikleri gerçek derleme ve koşularla anlatıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ve .NET 11 (ConfigureWebApplicationBuilder örneği), Microsoft.AspNetCore.Mvc.Testing 10.0.15 ve 11.0.0, xUnit.net v3 4.0.3, Testcontainers for .NET 4.16 ve Playwright 1.65 ile derlenip denenmiştir.
Önce Tahmin Et
Ekibe yeni katılan biri Bookstore'a bir integration testi eklemek istiyor ve iyi puan almış eski bir blog yazısından şu factory'yi kopyalıyor:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder() =>
WebHost.CreateDefaultBuilder().UseStartup<Startup>();
protected override TestServer CreateServer(IWebHostBuilder builder) => base.CreateServer(builder);
}
İlk build error CS0246: The type or namespace name 'Startup' could not be found ile duruyor; Bookstore'da Startup yok. Arkadaşımız test projesine boş sayılabilecek bir Startup class'ı ekliyor (AddRouting ve UseRouting), build uyarılarla geçiyor:
warning ASPDEPR008: 'WebHost' is obsolete: 'WebHost is obsolete. Use HostBuilder or WebApplicationBuilder instead. For more information, visit https://aka.ms/aspnet/deprecate/008.'
warning CS0672: Member 'CustomWebApplicationFactory.CreateServer(IWebHostBuilder)' overrides obsolete member 'WebApplicationFactory<Startup>.CreateServer(IWebHostBuilder)'. ...
warning ASPDEPR008: 'WebApplicationFactory<Startup>.CreateServer(IWebHostBuilder)' is obsolete: 'IWebHost, which this method uses, is obsolete. Use one of the overloads that takes an IServiceProvider instead. ...'
Bu factory ile GET /api/books isteği ne döner?
Cevap: Derleniyor, Ama Bookstore'u Test Etmiyor
Content root'u da elle verip çalıştırınca:
GET /api/books -> 404 NotFound
Factory, CreateWebHostBuilder override edildiği için Bookstore'un Program.cs'ini hiç çalıştırmadı. Elle eklenen Startup'tan, içinde endpoint olmayan başka bir uygulama kurdu ve testler ona istek attı. Uyarıları susturup "test yeşil" demek mümkün; ama yeşil olan şey Bookstore değil. Eski bir testi okurken ilk soru bu yüzden "bu factory hangi uygulamayı ayağa kaldırıyor?" olmalı. Bu yazı o soruyu sürüm sürüm cevaplıyor: WebApplicationFactory'nin .NET 6'dan 11'e değişimi, Testcontainers'ın 3'ten 4'e geçişi, Protractor'dan Playwright'a geçiş ve Playwright'ın son sürümlerinde test yazanı etkileyen değişiklikler.
WebApplicationFactory: .NET 6'dan 11'e
.NET 5 ve öncesinde uygulama bir Startup class'ıyla kuruluyordu ve factory'nin tip argümanı da oydu. .NET 6'nın minimal hosting modeliyle ortada factory'nin yakalayabileceği bir builder kalmadı; Program.cs top-level statement'lardan ibaret. Çözüm ASP.NET Core 6 Preview 6'da geldi: factory artık Program.cs'i gerçekten çalıştırıyor. HostFactoryResolver, hosting'in DiagnosticListener event'lerini dinliyor; uygulama host'u build ettiği anda onu yakalıyor ve uygulamanın Run'a geçmesini HostAbortedException fırlatarak durduruyor. Bu ayrıntı serinin iki sürprizini açıklıyor: 2. yazıda Program.cs'in Build öncesindeki kodunun testte de çalışması ve 7. yazıda yanlış Program'ı veren E2E (uçtan uca test) host'unun kendi entry point'ini tekrar tekrar çalıştırması.
| Sürüm | Değişiklik | Eski testte görünüşü |
|---|---|---|
| .NET 5 ve öncesi | WebApplicationFactory<Startup>, IWebHostBuilder | CreateWebHostBuilder override'ı, UseStartup |
| .NET 6 | minimal hosting; factory Program.cs'i çalıştırır | public partial class Program { } ya da InternalsVisibleTo |
| .NET 10 | Program bildirimini source generator üretir | elle yazılmış bildirim gereksiz (ASP0027) |
| .NET 10 | UseKestrel, StartServer | gerçek port için yazılmış elle Kestrel kodu |
| .NET 10 | WebHostBuilder ve WebHost obsolete (ASPDEPR004 ve ASPDEPR008 uyarıları) | CreateServer(IWebHostBuilder) override'ı uyarı verir |
| .NET 11 | ConfigureWebApplicationBuilder hook'u | Build öncesi configuration için yazılmış geçici çözümler |
.NET 6 ile 9 arasında Program class'ı varsayılan olarak internal olduğu için Learn iki yol öneriyordu: API projesinde test projesine InternalsVisibleTo vermek ya da Program.cs'in sonuna public partial class Program { } eklemek. .NET 10'da bu bildirimi bir source generator üretiyor; 1. yazıda gördüğümüz gibi elle yazılmış satır ASP0027 bilgisiyle işaretleniyor ve silinebiliyor.
.NET 10'un iki eklemesi de bu seride kullanıldı. UseKestrel ve StartServer 7. yazıdaki E2E host'unun temeli; .NET 10 öncesinde gerçek bir port için factory'nin CreateHost'unu override edip Kestrel'i elle başlatan, ağırlıklı olarak blog yazılarından kopyalanmış kodlar vardı. WebHostBuilder'ın obsolete olması ise yukarıdaki uyarıların kaynağı: Microsoft'un breaking change sayfasına göre değişiklik .NET 10'un RC (Release Candidate; yayın adayı) 1 sürümünde geldi ve WebHostBuilder için ASPDEPR004, WebHost için ASPDEPR008 uyarısı veriliyor. Factory'de IWebHostBuilder alan CreateServer overload'u da obsolete oldu; yerine IServiceProvider alan bir overload geldi. Uyarılar build'i durdurmuyor, ama TreatWarningsAsErrors açık bir projede eski bir factory artık derlenmiyor.
.NET 11'de ConfigureWebApplicationBuilder
2. yazının ana sorusu, test configuration'ının Program.cs'e ne zaman ulaştığıydı: ConfigureAppConfiguration ile verilen değer, Program.cs'in Build'den önce okuduğu yerlere ulaşmıyordu ve çözüm UseSetting ya da CreateHost'tu. .NET 11 bunun için factory'ye yeni bir hook ekledi. Küçük bir API'yle denedim; Program.cs bir ayarı Build'den önce okuyor:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Build'den önce okunan bir ayar: 2. yazıdaki sorunun aynısı
string searchMode = builder.Configuration["Catalog:SearchMode"] ?? "contains";
WebApplication app = builder.Build();
app.MapGet("/search-mode", () => searchMode);
app.Run();
public sealed class OldStyleFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureAppConfiguration(config =>
config.AddInMemoryCollection(new Dictionary<string, string?> { ["Catalog:SearchMode"] = "full-text" }));
}
public sealed class NewHookFactory : WebApplicationFactory<Program>
{
// .NET 11: WebApplication.CreateBuilder çağrısının içinde, Program.cs devam etmeden önce çalışır
protected override void ConfigureWebApplicationBuilder(IHostApplicationBuilder builder) =>
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?> { ["Catalog:SearchMode"] = "full-text" });
}
İki factory için de /search-mode'un "full-text" döndürmesini bekleyen iki test yazdım. .NET 11 SDK'sıyla:
Hook.Check.SearchModeTests.ConfigureAppConfiguration_value_arrives_too_late [FAIL]
Assert.Equal() Failure: Strings differ
↓ (pos 0)
Expected: "full-text"
Actual: "contains"
↑ (pos 0)
=== TEST EXECUTION SUMMARY ===
Hook.Check Total: 2, Errors: 0, Failed: 1, Skipped: 0, Not Run: 0, Time: 0,200s
Eski yol .NET 11'de de aynı davranıyor; yeni hook'la verilen değer ise Build'den önce görünüyor. API dokümanına göre hook, entry point'in WebApplication.CreateBuilder çağrısı sırasında, çok erken çağrılıyor. Adının geçmişi de eski blog yazılarını okurken işe yarıyor: hook .NET 11 Preview 6'da ConfigureHostApplicationBuilder adıyla geldi, Preview 7'de ConfigureWebApplicationBuilder olarak yeniden adlandırıldı. Preview 6 döneminde yazılmış bir örnek GA (genel kullanıma açılan kararlı) sürümde derlenmiyor. Bu seri .NET 10 LTS (Long Term Support; uzun süreli destek) üzerinde kaldığı için Bookstore'un factory'leri hâlâ UseSetting kullanıyor; .NET 11'e geçişte ilk sadeleşecek yer burası.
Testcontainers for .NET: 3.x'ten 4.x'e
Testcontainers'ın 4.0 sürümü Kasım 2024'te çıktı ve sonraki minor sürümlerle birlikte eski testleri etkileyen değişiklikler getirdi. GitHub release notlarından derlediklerim:
- 4.0: image adının parçalarını tutan
IImageproperty'leri Docker'ın adlandırmasına göre yeniden düzenlendi, Azure SQL Edge modülü kaldırıldı ve MSSQL (SQL Server) modülünün varsayılan image'ı güncellendi. - 4.1:
net6.0hedefi kaldırılıpnet9.0eklendi; Docker client'ı olarak Testcontainers ekibinin kendi Docker.DotNet fork'u kullanılmaya başlandı. - 4.8: wait strategy'ler varsayılan olarak container'ın Running durumunu bekliyor; başlangıçta kapanan bir container artık exception fırlatıyor.
- 4.9:
net10.0hedefi eklendi. - 4.10: modül builder'larının parametresiz constructor'ları obsolete oldu; image artık açıkça veriliyor.
Son madde eski testlerde en sık görülen uyarı. 3.x döneminin tipik satırı 4.16'da şöyle:
MsSqlContainer sql = new MsSqlBuilder().Build();
warning CS0618: 'MsSqlBuilder.MsSqlBuilder()' is obsolete: 'This parameterless constructor is obsolete and will be removed. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.'
Uyarının arkasında bir test kararı var: parametresiz builder, modülün o sürümdeki varsayılan image'ını kullanıyordu ve Testcontainers paketi güncellendiğinde testlerin hangi SQL Server sürümüne karşı koştuğu sessizce değişebiliyordu. 1. yazıdan beri kullandığımız new MsSqlBuilder("mcr.microsoft.com/mssql/server:2025-latest") bu kararı koda yazıyor. Daha da sıkı olmak istersen 2025-latest yerine belirli bir CU (Cumulative Update; SQL Server'ın toplu güncelleme paketi) etiketi ya da image digest'i kullanılabilir.
Protractor'dan Playwright'a
Angular'ın eski projelerinde e2e/ klasörü, protractor.conf.js ve Jasmine ile yazılmış testler bulunur. Angular ekibi Nisan 2021'de açtığı RFC'de (Request for Comments; topluluğa görüş için açılan tasarım önerisi) Protractor'un geleceğini anlattı: Angular 12 ile yeni projelere Protractor eklenmeyecek, geliştirme 2022 sonunda Angular 15 ile bitecek, Protractor Ağustos 2023'te ömrünün sonuna gelecekti. GitHub deposu bugün arşivlenmiş durumda. 6. yazıda gördüğümüz gibi ng e2e artık bir framework'le gelmiyor ve Playwright, Cypress, Nightwatch, WebdriverIO ya da Puppeteer için bir paket öneriyor.
Protractor'un ayırt edici özelliği Angular'la senkronizasyondu: waitForAngular, Zone.js üzerinden bekleyen HTTP istekleri ve timer'lar bitene kadar bekliyordu. Playwright'ta böyle bir Angular bağı yok; bekleme element'in durumuna (actionability) ve web-first assertion'lara dayanıyor. Bu, zoneless çalışan Angular 22 için de doğru model: Zone.js olmayan bir uygulamada "Angular'ın işi bitti mi?" sorusunun cevabı yok, ama "kullanıcının gördüğü değer bu mu?" sorusunun var. Eski bir testi taşırken karşılıklar kabaca şöyle:
| Protractor | Playwright Test | Not |
|---|---|---|
browser.get('/cart') | page.goto('/cart') | baseURL'e göre |
element(by.css('.book-card button')) | page.getByRole('button', { name: 'Add to cart' }) | CSS yerine rol ve ad |
by.model, by.binding | getByLabel, getByText | bu locator'lar zaten yalnız AngularJS içindi |
browser.wait(EC.visibilityOf(el)) | await expect(locator).toBeVisible() | web-first assertion |
browser.sleep(2000) | karşılığı yok | bekleyecek koşulu bul |
browser.waitForAngular() | karşılığı yok | auto-wait ve assertion'lar |
Testleri satır satır çevirmek yerine kullanıcı senaryolarını Playwright'ta yeniden yazmak çoğu zaman daha hızlı: Protractor testleri genelde CSS selector'larına ve browser.sleep'lere yaslanıyor, birebir çeviri bu zayıflıkları da taşıyor.
Playwright'ın Son Sürümleri: Testi Yazanı Etkileyen Değişiklikler
Playwright sık aralıklarla minor sürüm çıkarıyor ve her sürümün release notlarında bir "Breaking Changes" bölümü olabiliyor. Son sürümlerden eski testleri doğrudan etkileyenler:
1.58'de _react ve _vue selector'ları, :light soneki ve launch'ın devtools seçeneği kaldırıldı. Kaldırılan bir engine'i kullanan eski bir satır 1.65'te açıklayıcı bir hata vermiyor:
await page.locator('_react=BookCard').click();
Error: locator.click: TypeError: Cannot read properties of undefined (reading 'queryAll')
at InjectedScript._queryEngineAll (<anonymous>:7292:48)
at InjectedScript.querySelectorAll (<anonymous>:7279:30)
Call log:
- waiting for locator('_react=BookCard')
Hatanın kaynağını release notları olmadan bulmak zor; eski bir suite'i yükseltirken kodda _react=, _vue= ve :light geçen yerleri aramak hızlı bir ön kontrol.
1.61 iki yenilik getirdi: browserContext.credentials ile sanal bir WebAuthn doğrulayıcısı (passkey ile login'i gerçek donanım anahtarı olmadan test etmek için) ve page.localStorage, page.sessionStorage API'leri. 7. yazıdaki setup project'te token'ı page.evaluate ile localStorage'a yazdık; 1.61 ve sonrasında aynı işi page.localStorage.setItem yapıyor. 1.62'de storageState'e passkey'leri de kaydetme seçeneği geldi. 1.64'te cihaz tanımlarındaki screen değeri sayfaya iletilmeye başlandı; ...devices['Desktop Chrome'] kullanan projelerde window.screen ve media query'ler artık emüle edilen ekranı görüyor, bu da görsel test baseline'larını değiştirebiliyor.
1.65'in "Breaking Changes" listesi on madde. Eski page object'leri en çok etkileyen ilki: TypeScript class field'ları artık tsconfig.json'daki useDefineForClassFields ayarına uyuyor ve bu ayar ES2022 ve sonrası hedeflerde açık. Yaygın bir page object yazımı 1.65'te şöyle kaldı:
class CartPanel {
readonly total = this.page.getByText(/^Total:/);
constructor(private readonly page: Page) {}
}
TypeError: Cannot read properties of undefined (reading 'getByText')
2 |
3 | class CartPanel {
> 4 | readonly total = this.page.getByText(/^Total:/);
| ^
Field initializer artık constructor parametresinden oluşan page property'sinden önce çalışıyor. Release notlarının önerisi atamayı constructor'a taşımak ya da useDefineForClassFields'ı kapatmak. 7. yazıdaki CartPage locator'ları zaten constructor'da atadığı için etkilenmedi. Listenin diğer maddelerinden testleri etkileyebilecek olanlar: action'lar artık element'in inert olmamasını da bekliyor (6. yazıdaki actionability listesinin son maddesi), hiçbir shard'da test bulunamayan koşu "No tests found" ile kalıyor, cookie eşleştirme RFC 6265'e (HTTP cookie standardı) uyuyor, APIRequestContext'teki params adresteki mevcut query'nin yerine geçmek yerine ona ekleniyor ve tarayıcının render etmediği metinler artık accessible name'in parçası değil. Ayrıca 1.65 Node.js 20'de test edilmiyor; Node.js 22, 24 ya da 26 gerekiyor.
Yükseltmeyi Güvenli Yapmak
Eski bir suite'i okurken ya da araçları yükseltirken kullandığım sıra:
- Factory'nin hangi uygulamayı ayağa kaldırdığını doğrula: tip argümanı API assembly'sinden mi,
CreateWebHostBuilderya daCreateHostoverride'ı var mı? - Build'i uyarılarıyla oku: ASPDEPR004, ASPDEPR008, CS0618 ve ASP0027, eski API'lerin nerede durduğunu gösteriyor.
- Testcontainers modüllerinde image'ı açıkça ve sabit bir etiketle ver.
- Playwright'ı sürüm sürüm yükselt; her sürümün "Breaking Changes" bölümünü oku, tarayıcıları aynı anda güncelle.
- Yükseltmeden sonra suite'i
--repeat-eachile birkaç kez çalıştır ve flaky sayısını yükseltme öncesiyle karşılaştır. - Görsel test baseline'larını yükseltmeyle aynı pull request'te, CI image'ında güncelle ve diff'leri tek tek incele.
Üretimde Dikkat
Test araçlarını uygulama paketleriyle aynı disiplinle yükselt. Microsoft.AspNetCore.Mvc.Testing'in sürümü API'nin hedeflediği ASP.NET Core sürümüyle aynı ailede kalmalı; 11.0 paketi net11.0 içindir ve .NET 10 projesinde 10.0.x serisinde kalınmalı. Playwright'ın npm paketi ile Docker image etiketi aynı sürümü göstermeli; 8. yazıdaki görsel test baseline'ları v1.65.0-noble image'ına bağlı.
Sürüm farklarını izlemenin en ucuz yolu, araç sürümlerini tek yerde toplamak: .NET tarafında Central Package Management ya da en azından sabit sürüm numaraları, Angular tarafında package-lock.json ve ^'sız sürümler. Dependabot ya da Renovate gibi bir bot yükseltme pull request'lerini açabilir; ama Playwright gibi tarayıcı build'i getiren bir paketin pull request'inde E2E suite'in ve görsel testlerin gerçekten çalıştığından emin ol.
Preview sürümler için yazılmış örneklere dikkat et. .NET 11'in factory hook'u iki preview arasında ad değiştirdi; Playwright'ta da deneysel API'ler (experimental- önekli paketler gibi) kaldırılabiliyor. Bir blog örneğini kopyalamadan önce hangi sürümle yazıldığına bak.
Sık Düşülen Tuzaklar
Eski blog örneklerindeki Startup tabanlı factory'yi .NET 10'a kopyalamak. Yukarıda gördüğümüz gibi derlenmesi için eklenen bir Startup, testleri gerçek uygulamadan koparıyor ve testler yeşil kalıyor. Minimal hosting kullanan bir uygulamada factory'nin tip argümanı uygulamanın kendi assembly'sinden bir tip olmalı ve CreateWebHostBuilder override edilmemeli.
Obsolete uyarılarını #pragma ile susturmak. ASPDEPR kodlu uyarılar bir sonraki sürümde hataya dönüşebilecek API'leri işaretliyor; susturmak, yükseltmeyi bir sonraki ekibe bırakmak demek. Uyarıyı veren satır çoğu zaman yeni API'yle birkaç satırda değişiyor.
Protractor testlerini birebir çevirmek. browser.sleep'leri waitForTimeout'a, CSS selector'larını page.locator'a çeviren bir dönüşüm, 6. yazıdaki tuzakların hepsini yeni suite'e taşıyor.
Playwright'ı birkaç sürüm birden atlayıp hatalara tek tek bakmak. 1.58'den 1.65'e tek adımda geçen bir suite'te TypeError gibi açıklamasız hataların hangi sürümden geldiğini bulmak zor. Sürüm sürüm yükseltmek ya da en azından aradaki her sürümün release notlarını okumak, hatayı nedeniyle eşleştiriyor.
Küçük Bir Deneme
★☆☆ Bookstore'un auth.setup.ts dosyasında page.evaluate ile yapılan localStorage yazımını 1.61 ile gelen page.localStorage.setItem ile değiştir ve storageState dosyasının içeriğinin değişmediğini doğrula.
★★☆ Test projesinin csproj'una TreatWarningsAsErrors ekle ve yukarıdaki eski factory'yi derle. Hangi uyarılar hataya dönüştü? Factory'yi Bookstore'un Program'ını kullanacak şekilde yeniden yaz ve GET /api/books'un 200 döndüğünü gör.
★★★ Bookstore'u .NET 11 SDK'sıyla net11.0'a taşıyan bir branch aç: Microsoft.AspNetCore.Mvc.Testing'i 11.0'a yükselt, 2. yazıdaki UseSetting satırlarını ConfigureWebApplicationBuilder'a taşı ve integration ile E2E suite'lerinin süresini iki sürümde karşılaştır.
Seri burada bitiyor. Bookstore'un API'si gerçek SQL Server'la, sınırda taklit edilen dış servislerle, Angular'la arasındaki sözleşmeyle ve tarayıcıdan geçen uçtan uca akışlarla test altında; flaky testlere ve sürüm değişikliklerine karşı da bir yöntemimiz var. Aynı suite'i CI'da hızlı tutmak için GitHub Actions serisinin Playwright yazısına, yük altında nasıl davrandığını görmek için Performance ve Load Testing serisine bakabilirsin.