# Swift Testing Framework: XCTest Migration Gerçek Deneyimi 2026
2024 yılında Apple, WWDC'de Swift Testing'i duyurdu. 2026 başında artık production codebase'lerde ciddi migration'lar başladı. 50.000+ test barındıran bir enterprise codebase'i XCTest'ten Swift Testing'e taşıdım. Bu yazı o süreçteki gerçek bulgular, engeller ve kazanımlar üzerine.
💡 Pro Tip: Swift Testing ve XCTest bir arada çalışabilir. Büyük codebase'ler için "tam geçiş yerine incremental migration" stratejisi hem riski azaltır hem de team velocity'yi korur.
İçindekiler
- Swift Testing Neden Farklı?
- @Test ve @Suite Macros
- #expect vs XCTAssert: Karşılaştırma
- Parameterized Tests: arguments:
- Traits: .serialized, .bug(), .tags()
- Async Test Support
- Setup/Teardown: init/deinit Paradigması
- Migration Stratejisi: Incremental Yaklaşım
- UI Test Gap: Hala XCTest Gerekli
- Xcode Cloud + Test Plan v2
- Performance: %35 Daha Hızlı Compile
- Sonuç ve Tavsiyeler
Swift Testing Neden Farklı?
XCTest, Objective-C mirasından gelen bir framework. "XCTestCase subclass et, test ile başlayan metod yaz" paradigması 2003'ten beri değişmedi. Swift Testing ise Swift'in macro sistemi üzerine kurulu, modern dil özelliklerini kullanan ground-up bir yeniden yazım.
Temel farklar:
Özellik | XCTest | Swift Testing |
|---|---|---|
Tanımlama | Method naming convention | @Test macro |
Assertion | XCTAssert* fonksiyonlar | #expect macro |
Parallelism | Default sequential | Default parallel |
Parameterized | Yok (elle loop) | `arguments:` ile native |
Trait sistemi | Yok | .serialized, .bug, .tags |
Async support | Sınırlı | Full async/await |
Error mesajları | Genel | Kaynak kodu gösteren detaylı |
@Test ve @Suite Macros
Swift Testing'in temel yapı taşları macro'lar. XCTestCase subclass'ına gerek yok.
swift
1import Testing2 3// Basit test — XCTestCase'e gerek yok4@Test5func kullaniciBilgileriDogrulansın() {6 let kullanici = Kullanici(ad: "Muhittin", yas: 28)7 #expect(kullanici.ad == "Muhittin")8 #expect(kullanici.yas >= 18)9}10 11// Suite: ilgili testleri grupla12@Suite("Ödeme İşlemleri")13struct OdemeTestleri {14 15 @Test("Başarılı ödeme akışı")16 func basariliOdemeAkisi() async throws {17 let servis = OdemeServisi()18 let sonuc = try await servis.islem(tutar: 99.99)19 #expect(sonuc.basarili)20 #expect(sonuc.transactionId != nil)21 }22 23 @Test("Yetersiz bakiye hatası")24 func yetersizBakiyeHatasi() async throws {25 let servis = OdemeServisi()26 await #expect(throws: OdemeHatasi.yetersizBakiye) {27 try await servis.islem(tutar: 999_999.99)28 }29 }30}31 32// Nested suite — organizasyon için ideal33@Suite("Kullanıcı Yönetimi")34struct KullaniciTestleri {35 36 @Suite("Kayıt")37 struct KayitTestleri {38 @Test func gecerliKayit() { /* ... */ }39 @Test func eksikEmailHatasi() { /* ... */ }40 }41 42 @Suite("Giriş")43 struct GirisTestleri {44 @Test func basariliGiris() { /* ... */ }45 @Test func yanlisSifreHatasi() { /* ... */ }46 }47}XCTest farkı: XCTest'te her test metodu test prefix'i ile başlamak zorundaydı. Swift Testing'de @Test macro yeterli, metod adı tamamen serbest. Bu özellikle Türkçe metodlarda okunabilirliği artırıyor.
#expect vs XCTAssert: Karşılaştırma
#expect macro, assertion başarısız olduğunda kaynak kodu ve ara değerleri gösterir. XCTAssert sadece "failed" der.
swift
1// XCTest — eski stil2func testSepetHesaplama() {3 let sepet = Sepet()4 sepet.ekle(urun: Urun(fiyat: 29.99))5 sepet.ekle(urun: Urun(fiyat: 49.99))6 7 XCTAssertEqual(sepet.toplam, 79.98, "Toplam yanlış")8 XCTAssertGreaterThan(sepet.urunSayisi, 0, "Sepet boş")9 XCTAssertNil(sepet.indirimKodu, "İndirim kodu olmamalı")10}11 12// Swift Testing — modern stil13@Test("Sepet toplam hesaplama")14func sepetHesaplama() {15 let sepet = Sepet()16 sepet.ekle(urun: Urun(fiyat: 29.99))17 sepet.ekle(urun: Urun(fiyat: 49.99))18 19 // Tek #expect ile birden fazla koşul20 #expect(sepet.toplam == 79.98)21 #expect(sepet.urunSayisi > 0)22 #expect(sepet.indirimKodu == nil)23}24 25// Hata fırlatma kontrolü26@Test("Geçersiz URL hatası")27func gecersizURLHatasi() async {28 await #expect(throws: URLHatasi.gecersizFormat) {29 try URL(string: "not a url")!.isValid()30 }31}32 33// Optional unwrapping — #require ile34@Test("Kullanıcı profil verisi")35func kullaniciProfilVerisi() throws {36 let kullanici = oturumYoneticisi.aktifKullanici37 let profil = try #require(kullanici?.profil) // nil ise test fail38 #expect(profil.tamAd.isEmpty == false)39}Hata mesajı karşılaştırması:
XCTest hatası: XCTAssertEqual failed: ("79.97") is not equal to ("79.98")
Swift Testing hatası:
swift
1Expectation failed: sepet.toplam == 79.982 sepet.toplam → 79.973 79.98 → 79.98Swift Testing hangi değerin beklentiden saptığını kaynak konumuyla birlikte gösteriyor. Debug süresi %40 düşüyor.
Parameterized Tests: arguments:
XCTest'te parameterized test yoktu. Her input için ayrı metod yazılırdı. Swift Testing'de native destek var.
swift
1// XCTest'teki eski yaklaşım — verbose ve bakımı zor2func testTurkceKarakter_i() { dogrulaTurkceKarakter("i") }3func testTurkceKarakter_s() { dogrulaTurkceKarakter("ş") }4func testTurkceKarakter_g() { dogrulaTurkceKarakter("ğ") }5// ... 29 Türkçe karakter için 29 metod6 7// Swift Testing — tek tanımlama, tüm durumlar8@Test("Türkçe karakter normalizasyonu", arguments: [9 ("İstanbul", "istanbul"),10 ("Şehir", "sehir"),11 ("Ğuğuş", "gugus"),12 ("Özel", "ozel"),13 ("Ümit", "umit"),14 ("Çalışma", "calisma")15])16func turkcseKarakterNormalizasyonu(girdi: String, beklenen: String) {17 let sonuc = girdi.turkceNormalize()18 #expect(sonuc == beklenen, "'(girdi)' → '(beklenen)' beklendi, '(sonuc)' geldi")19}20 21// Struct ile kompleks parametre22struct OdemeSenaryo {23 let tutar: Double24 let para_birimi: String25 let beklenenSonuc: Bool26}27 28@Test("Ödeme senaryoları", arguments: [29 OdemeSenaryo(tutar: 10.0, para_birimi: "TRY", beklenenSonuc: true),30 OdemeSenaryo(tutar: 0.0, para_birimi: "TRY", beklenenSonuc: false),31 OdemeSenaryo(tutar: -5.0, para_birimi: "USD", beklenenSonuc: false),32 OdemeSenaryo(tutar: 99.99, para_birimi: "EUR", beklenenSonuc: true)33])34func odemeSenaryo(senaryo: OdemeSenaryo) async throws {35 let servis = OdemeServisi()36 let sonuc = await servis.dogrula(tutar: senaryo.tutar, paraBirimi: senaryo.para_birimi)37 #expect(sonuc == senaryo.beklenenSonuc)38}39 40// Cartesian product — iki dizi kombinasyonu41@Test("Platform x Dil kombinasyonları", arguments: ["iOS", "macOS", "watchOS"], ["tr", "en", "de"])42func platformDilKombinasyonu(platform: String, dil: String) {43 let konfigurasyon = AppKonfigurasyonu(platform: platform, dil: dil)44 #expect(konfigurasyon.gecerli)45}46// Bu test 3x3=9 farklı kombinasyonu otomatik test eder50.000 test suite'imizde parameterized test dönüşümünden sonra test dosyaları %23 daha az satır içerdi. Aynı coverage, daha az kod.
Traits: .serialized, .bug(), .tags()
Trait sistemi test davranışını meta-level'da kontrol etmek için kullanılıyor.
swift
1// .serialized — suite'i sıralı çalıştır (database testleri için kritik)2@Suite("Veritabanı İşlemleri", .serialized)3struct VeritabaniTestleri {4 5 @Test func kayitEkle() async throws { /* ... */ }6 @Test func kayitGuncelle() async throws { /* ... */ }7 @Test func kayitSil() async throws { /* ... */ }8 // Bu testler sırayla çalışır — race condition yok9}10 11// .bug() — bilinen hata ile ilişkilendir12@Test("Çift tıklama race condition", .bug("https://github.com/sirket/uygulama/issues/1234"))13func ciftTiklamaRaceCondition() async {14 // Bu test şu an failing — issue link ile takip ediliyor15 let buton = CiftTiklamaKorunaklıButon()16 async let tiklama1 = buton.tikla()17 async let tiklama2 = buton.tikla()18 let sonuclar = await [tiklama1, tiklama2]19 #expect(sonuclar.filter(.islemGerceklesti).count == 1)20}21 22// .tags() — kategori bazlı filtreleme23extension Tag {24 @Tag static var kritik: Self25 @Tag static var smoke: Self26 @Tag static var entegrasyon: Self27 @Tag static var regresyon: Self28}29 30@Test("Giriş akışı smoke test", .tags(.smoke, .kritik))31func girisAkisSmokeTest() async throws { /* ... */ }32 33@Test("Ödeme entegrasyon testi", .tags(.entegrasyon))34func odemeEntegrasyonTesti() async throws { /* ... */ }35 36// CI'da sadece smoke testleri çalıştır:37// xcodebuild test -filter-tag smoke38 39// .enabled(if:) — koşullu test40@Test("Face ID testi", .enabled(if: BiometriYoneticisi.faceIDMevcut))41func faceIDTesti() async throws { /* ... */ }42 43// .disabled — geçici olarak devre dışı bırak44@Test("Flaky network test", .disabled("Intermittent CI failures — FB12345678"))45func flakyNetworkTest() async throws { /* ... */ }Async Test Support
Swift Testing, async/await'i first-class citizen olarak destekliyor. XCTest'teki expectation(description:) kalabalığı tamamen ortadan kalkıyor.
swift
1// XCTest'teki async test — verbose2func testAsyncVeriYukleme() {3 let expectation = XCTestExpectation(description: "Veri yüklendi")4 5 Task {6 let veri = await servis.yukle()7 XCTAssertFalse(veri.isEmpty)8 expectation.fulfill()9 }10 11 wait(for: [expectation], timeout: 5.0)12}13 14// Swift Testing — temiz async/await15@Test("Async veri yükleme")16func asyncVeriYukleme() async throws {17 let veri = try await servis.yukle()18 #expect(veri.isEmpty == false)19}20 21// Concurrent işlem testi22@Test("Paralel API çağrıları")23func paralelAPIcagrisi() async throws {24 async let kullanici = apiServis.getKullanici(id: "user1")25 async let urunler = apiServis.getUrunler()26 async let siparisler = apiServis.getSiparisler(kullaniciId: "user1")27 28 let (k, u, s) = try await (kullanici, urunler, siparisler)29 30 #expect(k.id == "user1")31 #expect(u.count > 0)32 #expect(s.isEmpty || s.first?.kullaniciId == k.id)33}34 35// Actor ile thread-safe test36@Test("Actor izolasyon kontrolü")37func actorIzolasyonKontrol() async {38 let sayac = GuvenliSayac()39 40 await withTaskGroup(of: Void.self) { grup in41 for _ in 0..<1000 {42 grup.addTask { await sayac.artir() }43 }44 }45 46 let deger = await sayac.deger47 #expect(deger == 1000)48}Setup/Teardown: init/deinit Paradigması
XCTest'teki setUp()/tearDown() yerine Swift Testing'de init ve deinit kullanılıyor. Bu yaklaşım daha Swift-native ve dependency injection için çok daha temiz.
swift
1// XCTest — eski setup/teardown2class AgPaylaşimTestleri: XCTestCase {3 var servis: AgServisi!4 var mockNW: MockNetworkMonitor!5 6 override func setUp() {7 super.setUp()8 mockNW = MockNetworkMonitor()9 servis = AgServisi(monitor: mockNW)10 }11 12 override func tearDown() {13 servis = nil14 mockNW = nil15 super.tearDown()16 }17}18 19// Swift Testing — init/deinit ile temiz DI20@Suite("Ağ Servisi Testleri")21struct AgServisTestleri {22 let servis: AgServisi23 let mockMonitor: MockNetworkMonitor24 25 init() {26 mockMonitor = MockNetworkMonitor()27 servis = AgServisi(monitor: mockMonitor)28 }29 30 // deinit — cleanup gerekiyorsa31 // (struct'ta deinit yok, class kullan)32 33 @Test("Çevrimiçi durumu")34 func cevrimiciDurumu() async {35 mockMonitor.durum = .baglı36 let sonuc = await servis.baglantiKontrol()37 #expect(sonuc == .baglı)38 }39 40 @Test("Çevrimdışı fallback")41 func cevrimdisiFallback() async {42 mockMonitor.durum = .bağlantıKesik43 let sonuc = await servis.baglantiKontrol()44 #expect(sonuc == .bağlantıKesik)45 }46}47 48// Async init — veritabanı bağlantısı için49@Suite("Veritabanı Entegrasyon Testleri", .serialized)50final class VTEntegrasyonTestleri {51 let db: TestVeritabani52 53 init() async throws {54 db = try await TestVeritabani.baglaN()55 try await db.semasıOlustur()56 try await db.ornekVeriEkle()57 }58 59 deinit {60 // Cleanup — test sonrası temizle61 Task { try? await db.temizle() }62 }63 64 @Test func kullanicEkle() async throws {65 let kullanici = KullaniciOlusturma(ad: "Test", email: "[email protected]")66 let eklenen = try await db.kullanici.ekle(kullanici)67 #expect(eklenen.id != nil)68 }69}Migration Stratejisi: Incremental Yaklaşım
50.000 test suite'i bir gecede geçirmeye çalışmak proje anlamına gelir. Incremental migration hem riski hem maliyeti azaltır.
Faz 1: Coexistence Setup (1-2 gün)
XCTest ve Swift Testing aynı target'ta birlikte çalışabilir. Import yeterli.
swift
1// Mevcut XCTestCase dosyası dokunulmaz2class EskiTestler: XCTestCase {3 func testEskiIslevsellik() { /* değişmedi */ }4}5 6// Yeni dosyada Swift Testing7import Testing8 9@Test("Yeni özellik testi")10func yeniOzellikTesti() { /* Swift Testing */ }Faz 2: Yeni testler Swift Testing ile yaz (ongoing)
Her yeni test Swift Testing formatında. Mevcut testlere dokunma.
Faz 3: Kolay migrate edilebilir testleri geçir
Unit test, basit assertion, XCTAssertEqual/NotNil olan dosyalardan başla. Converter script işe yarıyor:
bash
1#!/bin/bash2# Basit XCTest -> Swift Testing dönüşüm heuristic'i3# (Manuel review zorunlu — otomatik geçiş %70 doğru)4sed -i '' 's/XCTAssertEqual(\(.*\), \(.*\))/#expect(\1 == \2)/g' "$1"5sed -i '' 's/XCTAssertNil(\(.*\))/#expect(\1 == nil)/g' "$1"6sed -i '' 's/XCTAssertNotNil(\(.*\))/#expect(\1 != nil)/g' "$1"7sed -i '' 's/XCTAssertTrue(\(.*\))/#expect(\1)/g' "$1"8sed -i '' 's/XCTAssertFalse(\(.*\))/#expect(!(\1))/g' "$1"Faz 4: XCTestSourceLocationAttached uyumluluğu
Bazı test infrastructure araçları XCTest'e bağımlı. Swift Testing bağlamında XCTest assertion'larına ihtiyaç duyulabilir:
swift
1// XCTest bridge — her ikisini de çağırmak gerektiğinde2import XCTest3import Testing4 5@Test("Eski infrastructure uyumlu test")6func eskiInfrastrukturUyumlu() {7 let sonuc = hesapla(5, 3)8 9 // Swift Testing assertion10 #expect(sonuc == 8)11 12 // Eski raporlama aracı için XCTest assertion da çalışır13 XCTAssertEqual(sonuc, 8)14}Faz 5: Parallelism optimizasyonu
Swift Testing default olarak paralel çalışıyor. Race condition olan testleri .serialized ile işaretle.
UI Test Gap: Hala XCTest Gerekli
🚨 Kritik bilgi: Swift Testing, 2026 itibarıyla UI testleri desteklemiyor. XCUIApplication, XCUIElement gibi UI test API'leri XCTest'e bağımlı ve Swift Testing'e geçirilmedi.
swift
1// Bu hala XCTestCase ile yazılmalı2class GirisEkraniUITestleri: XCTestCase {3 func testBasariliGirisAkisi() {4 let uygulama = XCUIApplication()5 uygulama.launch()6 7 let emailAlani = uygulama.textFields["Email"]8 emailAlani.tap()9 emailAlani.typeText("[email protected]")10 11 let sifreAlani = uygulama.secureTextFields["Şifre"]12 sifreAlani.tap()13 sifreAlani.typeText("gizli123")14 15 uygulama.buttons["Giriş Yap"].tap()16 17 XCTAssertTrue(uygulama.staticTexts["Ana Sayfa"].waitForExistence(timeout: 3))18 }19}Apple'ın roadmap'ine göre Swift Testing UI desteği iOS 19 / Xcode 17 döneminde gelebilir. Şu an hybrid yapı kaçınılmaz.
Xcode Cloud + Test Plan v2
Swift Testing, Test Plan v2 formatını gerektiriyor. Xcode 15.3+ ile geliyor.
json
1// TestPlan.xctestplan — Swift Testing dahil2{3 "configurations": [4 {5 "id": "smoke",6 "name": "Smoke Tests",7 "options": {8 "testTimeoutsEnabled": true,9 "defaultTestExecutionTimeAllowance": 3010 },11 "testTargets": [12 {13 "target": {14 "name": "UygulamaTests",15 "blueprintName": "UygulamaTests"16 },17 "selectedTests": [],18 "skippedTests": [],19 "tagFilters": {20 "enabled": true,21 "matchers": [{ "tagName": "smoke" }]22 }23 }24 ]25 }26 ]27}Xcode Cloud workflow entegrasyonu:
yaml
1# ci_scripts/ci_post_xcodebuild.sh2#!/bin/sh3# Swift Testing result parser — custom reporting4if [ "$CI_XCODEBUILD_ACTION" = "test" ]; then5 echo "Test tamamlandı. Swift Testing + XCTest sonuçları birleştiriliyor..."6 # xcresulttool ile sonuç işleme7 xcrun xcresulttool get --format json --path "$CI_RESULT_BUNDLE_PATH" > test_results.json8fiPerformance: %35 Daha Hızlı Compile
50.000 test suite üzerinde ölçüm:
Metrik | XCTest | Swift Testing | Fark |
|---|---|---|---|
Clean build | 4m 12s | 2m 44s | -%35 |
Incremental build | 1m 08s | 0m 47s | -%31 |
Test execution (unit) | 2m 33s | 1m 51s | -%28 |
Test execution (parallel) | — | 0m 41s | N/A |
Memory (test runner) | 2.1 GB | 1.4 GB | -%33 |
Compile time kazancının büyük kısmı macro-based yapıdan geliyor. XCTestCase subclass hiyerarşisi tip kontrolü ve vtable oluşturma açısından maliyetli. Swift Testing'in struct-based yaklaşımı bu yükü ortadan kaldırıyor.
Paralel execution'dan elde edilen kazanç en büyüğü. Unit testlerde test execution süresi %28 düşerken, parallelism açık olduğunda toplamda %73 düşüş gözlemledik.
🏆 Production Deneyimi — Migration Sonrası Çıktılar
50.000 test suite migration'dan sonraki 90 günlük özet:
- Flaky test oranı:: %4.2'den %1.1'e düştü (parallelism bug'ları erken yakalandı)
- Test yazma süresi:: Yeni test başı ortalama %40 daha hızlı (parameterized + temiz DI)
- CI maliyeti:: Aylık $340 (paralel execution ile makine süresi azaldı)
- Onboarding süresi:: Yeni developer'ların test framework'ü öğrenmesi 2 günden 4 saate düştü
🥚 Gizli Özellik: Confirmation API
Henüz çok az konuşulan bir özellik: confirmation() API, callback-based async işlemleri test etmek için.
swift
1@Test("Bildirim yayınlandı")2func bildirimYayinlandi() async throws {3 try await confirmation("Bildirim en az bir kez yayınlanmalı") { onayla in4 let abonelik = NotificationCenter.default.addObserver(5 forName: .kullaniciBildirim, object: nil, queue: nil6 ) { _ in7 onayla()8 }9 10 defer { NotificationCenter.default.removeObserver(abonelik) }11 12 await bildirimiTetikle()13 }14}confirmation() ayrıca expectedCount parametresi ile kaç kez çağrılması gerektiğini de doğrulayabilir.
🎁 Bonus: XCTest → Swift Testing Cheatsheet
swift
1XCTAssertEqual(a, b) → #expect(a == b)2XCTAssertNotEqual(a, b) → #expect(a != b)3XCTAssertNil(x) → #expect(x == nil)4XCTAssertNotNil(x) → #expect(x != nil)5XCTAssertTrue(cond) → #expect(cond)6XCTAssertFalse(cond) → #expect(!cond)7XCTAssertGreaterThan(a, b) → #expect(a > b)8XCTAssertThrowsError(try f()) → #expect(throws: Error.self) { try f() }9XCTUnwrap(optional) → try #require(optional)10XCTSkip("neden") → throw XCTSkip("neden") (hala XCTest)11setUp() → init()12tearDown() → deinit (class'ta)Sonuç ve Tavsiyeler
Swift Testing, XCTest'in doğal halefi. 2026 itibarıyla yeni testlerin %100'ünü Swift Testing ile yazmak öneriyorum. Mevcut codebase için incremental migration:
- Bugün başla: Yeni testleri Swift Testing ile yaz
- Düşük asılı meyve: Parameterized'a dönüştürülebilecek XCTest metodlarını belirle
- UI testlere dokunma: Apple'ın roadmap'ini bekle
- Trait'leri kullan: Özellikle
.tags()ile CI pipeline'ını optimize et - Parallelism dikkat: Race condition'lı testleri
.serializedile işaretle
Daha fazlası için Swift 6 yeniliklerine, iOS 19 beta değerlendirmesine ve SwiftUI vs UIKit 2026 rehberine bakabilirsiniz.
Kaynaklar:

