Swift Testing Rehberi 2026: XCTest'ten Geçiş, @Test Makrosu ve Modern Test Yazımı

Swift Testing, XCTest'in yerini alan modern framework. @Test makrosu, #expect kontrolleri, paralel çalıştırma ve @Suite ile Swift 6 ve Xcode 16'da birim testlerin nasıl yazıldığını, XCTest'ten kademeli geçiş örnekleriyle görün.

Swift Testing 2026: XCTest'ten Geçiş Rehberi

Güncellendi: 1 Temmuz 2026

Swift Testing, Apple'ın Xcode 16 ile kararlı hâle getirdiği ve XCTest'in yerini almak üzere tasarladığı modern test framework'üdür; @Test makrosu, #expect kontrolleri, paralel çalıştırma ve doğal async/await desteğiyle birim testleri hem daha okunabilir hem de belirgin biçimde daha hızlı hâle getirir. Objective-C'nin XCTestCase mirasını taşıyan sınıf tabanlı yapıyı geride bırakıp Swift'e özgü değer tipleri, makrolar ve Sendable güvenliği üzerine kurulan bu framework, iOS 18, macOS 15 ve Swift 6 ile birlikte varsayılan seçim hâline geldi.

  • Swift Testing, Xcode 16'dan itibaren proje şablonlarında varsayılan framework olarak yer alır; XCTest projelerle aynı hedefte yan yana çalışabilir.
  • @Test ve @Suite makroları, sınıf mirası yerine yapı (struct) tabanlı, izole test örnekleri sağlar.
  • #expect ve #require makroları, hata mesajlarında ifadeleri otomatik olarak yakalar; XCTAssertEqual zincirine ihtiyaç kalmaz.
  • @Test(arguments:) ile parametreli testler tek satırda tanımlanır ve her argüman ayrı bir çalıştırma olarak raporlanır.
  • Testler varsayılan olarak paralel ve async çalışır; Swift 6 concurrency modeliyle tam uyumludur.
  • XCTest'ten geçiş kademelidir: aynı hedefte iki framework birlikte derlenir ve testler tek tek taşınabilir.

Swift Testing nedir ve XCTest'ten farkı ne?

Swift Testing, açık kaynak olarak swiftlang/swift-testing deposunda geliştirilen ve Swift 6 ile Xcode 16 sürümünde Apple platformlarında birinci sınıf hâle gelen bir test framework'üdür. XCTest, Objective-C döneminden bu yana NSInvocation ve dinamik yöntem çözümlemesine dayanır; Swift Testing ise makrolar ve derleme zamanı üretilen kayıt tabloları üzerine kuruludur. Bu farklılık yüzeyde küçük görünse de test yazımında ciddi sonuçlar doğurur.

Somut olarak XCTest'te her test bir XCTestCase alt sınıfının örneği olarak koşar; sınıflar referans tiptir, testler arası izolasyon setUp/tearDown zinciriyle sağlanır. Swift Testing'de her test, @Test ile işaretlenmiş bağımsız bir fonksiyondur ve tipik olarak bir struct içinde yaşar. Değer tipleri sayesinde her testin kendi taze örneği vardır, paralel yürütmede yarış koşulu üretmez.

Diğer önemli farklar: XCTest'te başarısız bir XCTAssertEqual yalnızca "0 not equal to 1" gibi kuru bir mesaj döndürür; Swift Testing'te #expect(user.age == 18) başarısız olduğunda hem user.age hem de karşılaştırılan değer raporda görünür. Ayrıca Swift Testing testleri varsayılan olarak paralel çalışır; XCTest'te bu opt-in bir Xcode ayarıdır ve XCTestCase alt sınıfları arasında sınırlıdır. Framework'ün resmi tanıtımı için Apple'ın Swift Testing sayfasına bakabilirsiniz.

Kurulum ve ilk @Test fonksiyonu

Xcode 16'dan itibaren yeni bir iOS veya macOS projesi oluşturduğunuzda "Include Tests" seçili olduğunda Xcode, otomatik olarak Swift Testing tabanlı bir dosya üretir. Var olan projelerde ise test hedefinize import Testing eklemek yeterlidir; Swift Package Manager tabanlı paketlerde Package.swift dosyasına manuel bağımlılık eklemek gerekmez, çünkü swift-tools-version: 6.0 ve sonrasında framework toolchain'e gömülüdür.

En küçük çalışır örnek şudur:

import Testing
@testable import PriceKit

@Test func indirimTutariHesapla() {
    let indirimli = fiyatUygula(base: 100, discount: 0.2)
    #expect(indirimli == 80)
}

Görüldüğü gibi test bir sınıfın metodu değil, üst düzey (top-level) bir fonksiyondur. @Test makrosu Xcode'un test gezginine bu fonksiyonu kaydeder. Fonksiyona isterseniz insan tarafından okunabilir bir başlık verebilirsiniz — bu, XCTest'te sık başvurulan uzun metot adlarının yerine geçer:

@Test("%20 indirim, 100 TL'yi 80 TL yapar")
func indirimTutari() {
    #expect(fiyatUygula(base: 100, discount: 0.2) == 80)
}

Test raporunda bu başlık aynen görünür; Türkçe karakter, boşluk ve yüzde işareti gibi ifadeler sorunsuz kullanılabilir. Xcode Test Navigator'da fonksiyon adı yerine bu etiket listelenir, dolayısıyla test dosyanız hem koda hem ekibinizdeki QA süreçlerine daha net konuşur.

#expect ve #require makroları nasıl kullanılır?

Swift Testing'de iddia (assertion) mekanizması iki makrodan ibarettir: #expect ve #require. XCTest'in düzinelerce XCTAssert varyantını (XCTAssertEqual, XCTAssertGreaterThan, XCTAssertTrue...) tek bir ifade tabanlı makroya indirir. Karşılaştırma operatörlerini, opsiyonel açmayı ve boolean ifadeleri doğrudan yazarsınız; makro derleme zamanında ifadeyi parçalar ve hata durumunda operandları raporlar.

@Test func kullaniciDogrulama() throws {
    let user = try #require(User(email: "[email protected]"))
    #expect(user.email.contains("@"))
    #expect(user.age >= 18)
    #expect(user.roles == [.reader, .editor])
}

Aradaki fark şudur: #expect başarısız olsa bile test yürümeye devam eder ve tüm iddialar raporlanır. #require ise başarısızlıkta testi hemen sonlandırır; opsiyonel açma veya throws içeren senaryolarda güvenli bir "buradan sonra devam etmeye değmez" ifadesidir. Bu ayrım, birden çok küçük iddiayı tek bir testte gruplarken oldukça kullanışlıdır.

Hata bekleme (throwing) senaryolarında XCTAssertThrowsError yerine #expect(throws:) kullanılır:

@Test func gecersizEmailHataFirlatir() {
    #expect(throws: ValidationError.invalidEmail) {
        try User.parse(email: "yok")
    }
}

Belirli bir hata tipini beklemek isterseniz throws: ValidationError.self yazabilir, hiçbir hata beklemediğinizi belirtmek içinse throws: Never.self kullanabilirsiniz.

@Suite ile test organizasyonu

XCTest'te ortak kurulum kodu XCTestCase alt sınıfının setUp metodunda toplanırdı. Swift Testing bunun yerine @Suite ile işaretlenmiş bir struct veya actor önerir. Örneğin bir ödeme modülü için:

@Suite("Ödeme akışı")
struct PaymentFlowTests {
    let sut: PaymentProcessor

    init() {
        sut = PaymentProcessor(gateway: MockGateway())
    }

    @Test func basariliOdeme() async throws {
        let sonuc = try await sut.charge(amount: 250)
        #expect(sonuc.status == .succeeded)
    }

    @Test func yetersizBakiye() async {
        await #expect(throws: PaymentError.insufficientFunds) {
            try await sut.charge(amount: 999_999)
        }
    }
}

init, XCTest'teki setUp'ın karşılığıdır — her test için yeniden çağrılır, çünkü suite bir değer tipi olarak her testte yeni bir örnek üretir. Deinit ihtiyacınız varsa deinit yazın; tearDown ayrı bir metoda gerek kalmaz. İç içe suite'ler de mümkündür: @Suite ile işaretli bir yapının içinde başka bir @Suite struct tanımlayabilir, testleri hiyerarşik olarak gruplayabilirsiniz.

Modern durum yönetimini test etmeye gelince, benim tercihim yeni @Observable API'sini test etmek için suite başına bir view-model örneği kurmaktır. Bu konuya daha derin girmek isterseniz SwiftData ve @Query kalıcılık rehberimizde paylaşılan kalıcılık katmanı örneğine bakabilirsiniz.

Parametreli testler ve @Test(arguments:)

Parametreli testler, XCTest'in belki de en çok özlenen eksiğiydi; topluluk yıllarca for döngüsü içinde XCTAssert yazarak bu boşluğu doldurmaya çalıştı. Swift Testing bunu birinci sınıf bir özellik olarak sunar. @Test(arguments:) ile bir koleksiyon geçirdiğinizde framework, her eleman için ayrı bir test çalıştırması üretir ve her biri raporda tek tek görünür:

@Test(arguments: [
    ("[email protected]", true),
    ("nope",            false),
    ("",                false),
    ("[email protected]",          true)
])
func emailDogrulama(email: String, gecerli: Bool) {
    #expect(EmailValidator.isValid(email) == gecerli)
}

Birden fazla parametre geçirmek isterseniz iki ayrı diziyi arguments: içine iki argüman olarak verirsiniz; framework bunları Kartezyen çarpım olarak çalıştırır. Bu, farklı Locale ve para birimi kombinasyonlarını test ederken çok değerlidir:

@Test(arguments: ["tr_TR", "en_US", "de_DE"], [0.0, 99.99, 1234.5])
func fiyatBicimlendirme(locale: String, tutar: Double) {
    let formatter = PriceFormatter(locale: Locale(identifier: locale))
    #expect(!formatter.format(tutar).isEmpty)
}

Xcode 16.2 ve sonrasında Test Navigator, parametreli çalıştırmaları tek testin altında bir alt liste olarak gösterir. Sadece başarısız olan varyantı yeniden çalıştırabilir, ya da tek bir varyantı fare sağ tuşuyla debug'a alabilirsiniz.

Trait, tag ve koşullu çalıştırma

Trait'ler, testlere davranış eklemenin bildirimsel yoludur. En sık kullanılanlar: .tags(...), .disabled(...), .enabled(if:), .bug(...), .timeLimit(.minutes(1)) ve .serialized. Örneğin bilinen bir Radar hatasına bağlı, geçici olarak devre dışı bırakılmış bir testi şöyle belgeleyebilirsiniz:

@Test(
    "Kupon kodu büyük/küçük harfe duyarsız olmalı",
    .bug("FB13579246", "Kupon normalizasyonu yeniden yazılana kadar devre dışı"),
    .disabled("Ana branch'te çakışan davranış çözülene kadar")
)
func kuponKoduBoyutDuyarsiz() { /* ... */ }

Etiketler (.tags) test filtrelemede kritiktir. CI'da her PR'da yalnızca hızlı testleri çalıştırıp gecelik pipeline'da tümünü koşmak yaygın bir örüntüdür:

extension Tag { @Tag static var slow: Self; @Tag static var integration: Self }

@Test(.tags(.slow, .integration))
func buyukVeriKumesiIslenir() async throws { /* ... */ }

Bu etiketleri komut satırından filtrelemek için swift test --filter tag:slow veya xcodebuild test -only-testing:... kullanabilirsiniz. Belirli bir OS sürümüne bağlı testler için .enabled(if:) koşuluyla #available kontrolü yapmak, testin yanlış platformlarda başarısız görünmesini önler.

Async, actor ve Swift 6 concurrency

Swift Testing, baştan itibaren async/await'i gözeterek tasarlandı. Test fonksiyonlarını doğrudan async throws yazabilir, XCTestExpectation ve wait(for:timeout:) jimnastiğini tümüyle atlayabilirsiniz. Objective-C KVO tabanlı expectation(forKeyPath:) gibi eski API'leri hatırlayanlar için bu ferahlatıcı bir sadeleşmedir.

@Test func kullaniciYuklenir() async throws {
    let repo = UserRepository()
    let user = try await repo.load(id: 42)
    #expect(user.name == "Ada")
}

Swift 6 strict concurrency açıkken suite'in kendisi bir aktör olabilir. UI ile ilgili view-model'leri test ediyorsanız @MainActor ile işaretlemek en temiz yoldur; framework, her testi otomatik olarak doğru izolasyon bağlamında çağırır. Sendable ve veri yarışları konusundaki ayrıntılar için Swift 6 Strict Concurrency ve Sendable rehberimize göz atabilirsiniz.

@MainActor
@Suite struct ProfileViewModelTests {
    @Test func adDegistiginde_baslikGuncellenir() async {
        let vm = ProfileViewModel(user: .stub)
        vm.name = "Turing"
        #expect(vm.title == "Turing")
    }
}

Paralellik varsayılan olarak açıktır; sırayla çalışması gereken bir suite için .serialized trait'ini ekleyin. Bu genellikle paylaşılan bir SQLite dosyasına veya UserDefaults'a dokunan entegrasyon testlerinde gereklidir.

XCTest'ten Swift Testing'e adım adım geçiş

Geçiş için "bir gecede taşı" tavsiye etmiyorum. Deneyimime göre en pürüzsüz yol şudur: (1) test hedefine import Testing ekleyin ve mevcut XCTest'i kaldırmadan yeni testleri Swift Testing ile yazmaya başlayın. (2) Yeni bir modül veya feature branch açıldığında, o modülün testlerini @Test olarak yazın. (3) Eski XCTest sınıflarını fırsat buldukça küçük gruplar hâlinde dönüştürün.

Sık karşılaşılan dönüşüm örüntüleri:

  • XCTAssertEqual(a, b)#expect(a == b)
  • XCTAssertTrue(cond)#expect(cond)
  • XCTAssertNil(x)#expect(x == nil)
  • XCTUnwrap(x)try #require(x)
  • XCTAssertThrowsError(try f())#expect(throws: Error.self) { try f() }
  • XCTSkip("neden") → suite/test üzerinde .disabled("neden") trait'i
  • setUp/tearDowninit/deinit (struct suite)

UI testleri (XCUIApplication) ve measure tabanlı performans testleri şu an için Swift Testing kapsamı dışındadır; onları XCTest hedefinde tutmaya devam edin. Xcode 16, tek bir test hedefinde iki framework'ün birlikte derlenmesini sorunsuz destekler ve Test Navigator ikisini de yan yana listeler.

CI, Xcode Cloud ve komut satırı

Aslına bakarsanız, CI tarafında değişen çok az şey var. xcodebuild test Swift Testing testlerini otomatik olarak keşfeder; ek bir bayrak gerekmez. Xcode Cloud, xUnit uyumlu bir raporlayıcıya varsayılan olarak yazar; GitHub Actions veya GitLab CI ile çalışıyorsanız swift test --parallel --xunit-output results.xml komutuyla JUnit tabanlı raporlar üretebilir, mevcut yorumlayıcılarınıza besleyebilirsiniz.

Ayrıntılı çıktı almak için Swift Package Manager'da yeni gelen swift test --enable-swift-testing anahtarı 6.0'dan itibaren varsayılandır; 5.10'da hâlâ opt-in olduğundan eski toolchain'lerde bunu manuel eklemeniz gerekir. Aynı depoyu birden çok Swift sürümüne karşı test ediyorsanız, resmi swift-testing GitHub deposundaki matris örneklerine bakmanızı öneririm; orada da GitHub Actions için iyi yapılandırılmış bir workflow bulacaksınız.

Xcode 16'nın yeni Test Navigator özellikleri (etiket bazlı gruplama, parametre bazlı yeniden çalıştırma, hata mesajlarında zenginleştirilmiş ifade gösterimi) Xcode ile en verimli iş akışı için kritiktir. Xcode 16.2'nin sürüm notlarına Apple'ın resmi Xcode sürüm notlarından ulaşabilirsiniz. Ajan tabanlı testleri Xcode içinde çalıştırmayı da tartıştığımız Xcode 26.3 ajan tabanlı kodlama rehberi, CI/CD tarafını tamamlayan iyi bir referanstır.

Sıkça Sorulan Sorular

Swift Testing, XCTest'in yerini tamamen mi alıyor?

Birim testleri için evet; Apple, yeni proje şablonlarında varsayılanı Swift Testing yaptı. Ancak UI testleri (XCUIApplication) ve performans testleri (measure) hâlâ XCTest üzerinde çalışıyor, bu API'ler için XCTest ihtiyaç duyulacak.

Swift Testing hangi minimum sürümleri gerektirir?

Xcode 16, Swift 6.0 ve Apple platformlarında iOS 16 / macOS 13 çalıştırma zamanı yeterlidir. Framework, geriye uyumluluk katmanlarıyla eski platformlarda da çalışacak biçimde tasarlanmıştır; Linux ve Windows'ta da resmi olarak desteklenir.

XCTest ve Swift Testing aynı hedefte birlikte çalışır mı?

Evet. Aynı test hedefine hem import XCTest hem import Testing yazabilir, iki framework testlerini yan yana çalıştırabilirsiniz. Kademeli geçiş için önerilen yaklaşım da budur.

Swift Testing testleri gerçekten paralel mi koşar?

Varsayılan olarak evet; framework, testleri Swift concurrency görevleri olarak zamanlar ve mümkün olduğunca eşzamanlı çalıştırır. Sıralı çalıştırma isterseniz .serialized trait'ini suite'e ekleyin.

Objective-C kodunu Swift Testing ile test edebilir miyim?

Doğrudan Objective-C'den @Test yazamazsınız, ancak Objective-C API'lerinizi Swift üzerinden çağırıp Swift Testing altında test edebilirsiniz. Saf Objective-C test dosyalarınız XCTest'te kalmaya devam etmelidir.

Lukas Müller
Yazar Hakkında Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.