Tüm Yazılar
KategoriTesting
Okuma Süresi
14 dk okuma
Yayın Tarihi
...
Kelime Sayısı
2.386kelime

Kahveni hazırla - bu içerikli bir makale!

Swift Testing macro-based framework, @Test, #expect, parameterized tests, traits ve XCTest'ten migration — 50.000+ test suite içeren production codebase deneyimi.

Swift Testing Framework: XCTest Migration Gerçek Deneyimi 2026

# 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ı?

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 Testing
2 
3// Basit test — XCTestCase'e gerek yok
4@Test
5func 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 grupla
12@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 ideal
33@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 stil
2func 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 stil
13@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şul
20 #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 ile
34@Test("Kullanıcı profil verisi")
35func kullaniciProfilVerisi() throws {
36 let kullanici = oturumYoneticisi.aktifKullanici
37 let profil = try #require(kullanici?.profil) // nil ise test fail
38 #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.98
2 sepet.toplam → 79.97
3 79.9879.98

Swift 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ı zor
2func testTurkceKarakter_i() { dogrulaTurkceKarakter("i") }
3func testTurkceKarakter_s() { dogrulaTurkceKarakter("ş") }
4func testTurkceKarakter_g() { dogrulaTurkceKarakter("ğ") }
5// ... 29 Türkçe karakter için 29 metod
6 
7// Swift Testing — tek tanımlama, tüm durumlar
8@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 parametre
22struct OdemeSenaryo {
23 let tutar: Double
24 let para_birimi: String
25 let beklenenSonuc: Bool
26}
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 kombinasyonu
41@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 eder

50.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 yok
9}
10 
11// .bug() — bilinen hata ile ilişkilendir
12@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 ediliyor
15 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ı filtreleme
23extension Tag {
24 @Tag static var kritik: Self
25 @Tag static var smoke: Self
26 @Tag static var entegrasyon: Self
27 @Tag static var regresyon: Self
28}
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 smoke
38 
39// .enabled(if:) — koşullu test
40@Test("Face ID testi", .enabled(if: BiometriYoneticisi.faceIDMevcut))
41func faceIDTesti() async throws { /* ... */ }
42 
43// .disabled — geçici olarak devre dışı bırak
44@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 — verbose
2func 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/await
15@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 testi
22@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 test
36@Test("Actor izolasyon kontrolü")
37func actorIzolasyonKontrol() async {
38 let sayac = GuvenliSayac()
39 
40 await withTaskGroup(of: Void.self) { grup in
41 for _ in 0..<1000 {
42 grup.addTask { await sayac.artir() }
43 }
44 }
45 
46 let deger = await sayac.deger
47 #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/teardown
2class 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 = nil
14 mockNW = nil
15 super.tearDown()
16 }
17}
18 
19// Swift Testing — init/deinit ile temiz DI
20@Suite("Ağ Servisi Testleri")
21struct AgServisTestleri {
22 let servis: AgServisi
23 let mockMonitor: MockNetworkMonitor
24 
25 init() {
26 mockMonitor = MockNetworkMonitor()
27 servis = AgServisi(monitor: mockMonitor)
28 }
29 
30 // deinit — cleanup gerekiyorsa
31 // (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ıKesik
43 let sonuc = await servis.baglantiKontrol()
44 #expect(sonuc == .bağlantıKesik)
45 }
46}
47 
48// Async init — veritabanı bağlantısı için
49@Suite("Veritabanı Entegrasyon Testleri", .serialized)
50final class VTEntegrasyonTestleri {
51 let db: TestVeritabani
52 
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ı temizle
61 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ı dokunulmaz
2class EskiTestler: XCTestCase {
3 func testEskiIslevsellik() { /* değişmedi */ }
4}
5 
6// Yeni dosyada Swift Testing
7import Testing
8 
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/bash
2# Basit XCTest -> Swift Testing dönüşüm heuristic'i
3# (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ğinde
2import XCTest
3import Testing
4 
5@Test("Eski infrastructure uyumlu test")
6func eskiInfrastrukturUyumlu() {
7 let sonuc = hesapla(5, 3)
8 
9 // Swift Testing assertion
10 #expect(sonuc == 8)
11 
12 // Eski raporlama aracı için XCTest assertion da çalışır
13 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 dahil
2{
3 "configurations": [
4 {
5 "id": "smoke",
6 "name": "Smoke Tests",
7 "options": {
8 "testTimeoutsEnabled": true,
9 "defaultTestExecutionTimeAllowance": 30
10 },
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.sh
2#!/bin/sh
3# Swift Testing result parser — custom reporting
4if [ "$CI_XCODEBUILD_ACTION" = "test" ]; then
5 echo "Test tamamlandı. Swift Testing + XCTest sonuçları birleştiriliyor..."
6 # xcresulttool ile sonuç işleme
7 xcrun xcresulttool get --format json --path "$CI_RESULT_BUNDLE_PATH" > test_results.json
8fi

Performance: %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 in
4 let abonelik = NotificationCenter.default.addObserver(
5 forName: .kullaniciBildirim, object: nil, queue: nil
6 ) { _ in
7 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:

  1. Bugün başla: Yeni testleri Swift Testing ile yaz
  2. Düşük asılı meyve: Parameterized'a dönüştürülebilecek XCTest metodlarını belirle
  3. UI testlere dokunma: Apple'ın roadmap'ini bekle
  4. Trait'leri kullan: Özellikle .tags() ile CI pipeline'ını optimize et
  5. Parallelism dikkat: Race condition'lı testleri .serialized ile işaretle

Daha fazlası için Swift 6 yeniliklerine, iOS 19 beta değerlendirmesine ve SwiftUI vs UIKit 2026 rehberine bakabilirsiniz.

Kaynaklar:

Etiketler

#Swift Testing#XCTest#Testing#Migration#Macros#iOS#2026
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz