Swift Testing’i 2 projede kullanıyorum. XCTest’ten farkını deneyimledikten sonra yeni projelerde varsayılan olarak bunu seçiyorum. Neden olduğunu somut kazançlarla anlatayım.
1. Parametrized test gerçekten çalışıyor
XCTest’te parametrize etmek için ya XCTExpectFailure ile oynuyordun ya da loop içine assert koyuyordun. Hata mesajında hangi input’un düştüğü belirsiz kalıyordu.
@Test(arguments: [
("user@example.com", true),
("invalid", false),
("a@b.co", true),
("@missing.com", false)
])
func emailValidation(input: String, expected: Bool) {
#expect(isValidEmail(input) == expected)
}Xcode test navigator her satırı ayrı test olarak gösteriyor. Hangi input’un düştüğünü görmek için stack trace karıştırman gerekmiyor.
2. Tag ile test gruplama
Smoke test, integration test, slow test ayrımı için XCTest’te test class’larını veya filename convention kullanıyordun. Swift Testing’de tag atıyorsun:
extension Tag {
@Tag static var smoke: Self
@Tag static var slow: Self
}
@Test(.tags(.smoke))
func homeScreenLoads() { /* ... */ }
@Test(.tags(.slow))
func fullSyncFlow() async throws { /* ... */ }CI’da sadece smoke çalıştırmak için xcodebuild‘a tag filter veriyorsun. PR feedback loop’u 12 dakikadan 3 dakikaya düştü.
3. #expect diff output’u okunabilir
XCTAssertEqual iki Codable struct eşitsizliğinde tüm yapıyı tek satırda yazıyordu. Hangi field farklıydı bulmak için print etmek zorunda kalıyordun.
#expect(actualUser == expectedUser)Test failed output’unda fark eden field’lar diff formatında görünüyor. Nested struct bile destekleniyor.
4. Async test doğal
XCTest’te async testler XCTestExpectation ve wait(for:timeout:) üzerinden yönetiliyordu. Swift Testing’de sadece async throws yazıyorsun:
@Test func fetchesUser() async throws {
let user = try await api.fetchUser(id: 42)
#expect(user.email == "user@example.com")
}Timeout için .timeLimit trait var:
@Test(.timeLimit(.seconds(5)))
func longOperation() async throws { /* ... */ }5. Suite isolation ve dependency injection
Test class’ı yerine struct veya actor kullanabiliyorsun. Her test run’ı için yeni instance yaratılıyor; shared state tuzağına düşme olasılığın azalıyor:
@Suite struct UserRepositoryTests {
let db: InMemoryDatabase
let repo: UserRepository
init() {
db = InMemoryDatabase()
repo = UserRepository(db: db)
}
@Test func savesUser() throws {
let user = User(id: 1, email: "x@y.z")
try repo.save(user)
#expect(db.users.count == 1)
}
}Geçiş maliyeti
Aynı target’ta XCTest ve Swift Testing yan yana yaşıyor. Hızlı migration yapmana gerek yok; yeni testleri Swift Testing’de yazıyorsun, eski testler dokunulana kadar bekliyor. 400 testlik bir projenin %30’unu taşıması 2 gün sürdü.
Hangi durumda XCTest’te kalırım
UI testler henüz Swift Testing’e taşınmadı; XCUIApplication kullanıyorsan XCTest şart. Çok eski Xcode sürümü destekliyorsan (Xcode 15 öncesi) hedefin yok. Bunun dışında yeni projede sebep göremiyorum.
Uyarı: Xcode test UI bazen takılıyor
Parametrized test sayısı 50’yi geçtiğinde Xcode’un test navigator’u yavaşlıyor. Benim stratejim: aşırı parametrize etmek yerine representative cases seçmek. 1000 case’lik fuzz test için ayrı bir target açıp CLI’dan çalıştırmak daha sağlıklı.
Son tavsiye
Yeni Swift projenin ilk test’ini Swift Testing’de yaz. Bir süre sonra XCTest’in verbose API’sine dönemezsin. Keyifli bir iş olması iyi bir işaret; test yazmayı caydıran friction’u azaltıyor.