2024 yılında Apple, WWDC'de Swift Testing'i duyurdu. 2026 başında artık production codebase'lerde ciddi migration'lar başladı. Bu yazı, büyük bir test suite'ini XCTest'ten Swift Testing'e taşırken karşına çıkacak kararları, engelleri ve ölçmen gereken kazanımları ele alıyor.
💡 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: Neyi Ölçmeli?
- 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.
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.
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ı:
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. Bu da hatanın kaynağını bulmayı belirgin şekilde hızlandırıyor.
Parameterized Tests: arguments:
XCTest'te parameterized test yoktu. Her input için ayrı metod yazılırdı. Swift Testing'de native destek var.
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 ederTekrarlayan test metodlarını parameterized test'e dönüştürmek aynı coverage'ı daha az satırla sağlar.
Traits: .serialized, .bug(), .tags()
Trait sistemi test davranışını meta-level'da kontrol etmek için kullanılıyor.
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.
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.
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: "test@test.com")66 let eklenen = try await db.kullanici.ekle(kullanici)67 #expect(eklenen.id != nil)68 }69}Migration Stratejisi: Incremental Yaklaşım
Binlerce testlik bir suite'i bir gecede geçirmeye çalışmak başlı başına bir 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.
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:
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:
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.
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("test@example.com")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.
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:
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: Neyi Ölçmeli?
Migration öncesi ve sonrası kendi suite'inde karşılaştırman gereken metrikler:
Metrik | Neden önemli | Nasıl ölçülür |
|---|---|---|
Clean build | Macro expansion ve test target'ının derleme maliyeti | xcodebuild clean build-for-testing süresi |
Incremental build | Günlük geliştirme döngüsü | Tek dosya değişikliği sonrası build-for-testing süresi |
Test execution (seri) | Karşılaştırma için taban çizgisi | xcodebuild test-without-building süresi |
Test execution (paralel) | Swift Testing'in varsayılan paralel çalıştırması | Aynı komut, paralel çalıştırma açıkken |
Memory (test runner) | CI makinesinin boyutlandırılması | CI agent metrikleri ya da Instruments |
Compile time tarafında beklenti, XCTestCase subclass hiyerarşisinin (tip kontrolü ve vtable maliyeti) struct-based yapıyla hafiflemesi; ama macro expansion da maliyet ekler, bu yüzden sonucu kendi suite'inde ölç.
Swift Testing testleri varsayılan olarak paralel çalıştırır ve kazancın en büyük kısmı genelde buradan gelir; paylaşılan state'e dokunan testleri ise .serialized ile işaretlemen gerekir.
🏆 Migration Sonrası İzlenecek Çıktılar
Geçişin işe yarayıp yaramadığını görmek için ilk birkaç ay şu göstergeleri takip et:
- Flaky test oranı: paralel çalıştırma gizli paylaşılan-state bug'larını erken yüzeye çıkarır; oran önce artıp sonra düşebilir
- Test yazma süresi: parameterized test ve temiz DI ile yeni test başına harcanan süre
- CI maliyeti: paralel execution'ın makine süresine etkisi
- Onboarding süresi: yeni geliştiricilerin test framework'üne alışma süresi
🥚 Gizli Özellik: Confirmation API
Henüz çok az konuşulan bir özellik: confirmation() API, callback-based async işlemleri test etmek için.
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
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:
Etiketler
iOS Geliştirme Haberleri
Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.
Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik
