Swift Testing Framework Mülakat 2026: #expect ve #require Makroları XCTest Karşısında

iOS mülakatları için yeni Swift Testing Framework'ünde uzmanlaş: #expect ve #require makroları, XCTest geçişi, ileri seviye desenler ve sık yapılan hatalar.

iOS mülakatları için #expect ve #require makrolarıyla Swift Testing Framework

WWDC 2024'te tanıtılan ve Swift 6 ile Xcode 16 ile birlikte gelen Swift Testing, Swift'te testlerin nasıl çalıştığına dair tam bir yeniden tasarım sunar. Bu framework, XCTest'in 40'tan fazla assertion'ını yalnızca iki makroyla değiştirir: #expect ve #require. Mülakatçılar bu bilgiyi artık iOS teknik mülakatlarında düzenli olarak ölçer.

Rehber Formatı

Her bölüm bir teknik mülakat sorusunu detaylı yanıtlar ve çalışan kodla yansıtır. İlerleme temel kavramlardan ileri seviye desenlere doğru gerçekleşir.

Swift Testing Temelleri

Soru 1: Swift Testing ile XCTest arasındaki temel farklar nelerdir?

Swift Testing, XCTest'e kıyasla beş temel değişiklik getirir:

  1. Bildirimsel sözdizimi: test öneki yerine @Test özniteliği
  2. İki evrensel makro: #expect ve #require, 40'tan fazla assertion'ın yerini alır
  3. Varsayılan paralel çalışma: tüm testler eş zamanlı yürütülür
  4. Yerel async desteği: Swift Concurrency ile tam entegrasyon
  5. Çapraz platform: Apple platformları, Linux ve Windows'ta çalışır
TestComparison.swiftswift
import XCTest
import Testing

// ❌ Legacy XCTest pattern
class UserServiceXCTests: XCTestCase {
    // Must start with "test"
    func testUserCreation() {
        let user = User(name: "Alice", age: 25)

        // Multiple verbose assertions
        XCTAssertNotNil(user)
        XCTAssertEqual(user.name, "Alice")
        XCTAssertGreaterThan(user.age, 18)
        XCTAssertTrue(user.isValid)
    }
}

// ✅ Modern Swift Testing pattern
@Test("User creation with valid data")
func userCreation() {
    let user = User(name: "Alice", age: 25)

    // Single macro for all verifications
    #expect(user.name == "Alice")
    #expect(user.age > 18)
    #expect(user.isValid)
}

Temel fark ifade gücünde yatar: Swift Testing, özelleşmiş assertion'lar yerine standart Swift ifadelerini kullanır; bu da testleri daha okunabilir, hata mesajlarını daha bilgilendirici kılar.

Soru 2: #expect makrosu nasıl çalışır?

#expect makrosu bir boolean ifadenin doğru olup olmadığını doğrular. Detaylı hata mesajları sağlamak için değerlendirilen değerleri otomatik olarak yakalar. XCTAssert'ın aksine yerel Swift sözdizimini kullanır.

ExpectMacroBasics.swiftswift
import Testing

@Test func basicExpectations() {
    let numbers = [1, 2, 3, 4, 5]
    let user = User(name: "Bob", email: "bob@example.com")

    // Simple comparisons - standard Swift expression
    #expect(numbers.count == 5)
    #expect(user.name == "Bob")

    // Comparisons with operators
    #expect(numbers.first! < numbers.last!)
    #expect(user.email.contains("@"))

    // Nil checking
    #expect(numbers.first != nil)

    // Collection verification
    #expect(numbers.contains(3))
    #expect(!numbers.isEmpty)
}

@Test func expectWithCustomMessage() {
    let balance = 150.0
    let withdrawAmount = 200.0

    // Custom message to clarify intent
    #expect(
        balance >= withdrawAmount,
        "Insufficient funds: balance \(balance) < withdrawal \(withdrawAmount)"
    )
}

Bir #expect başarısız olduğunda test yürütülmeye devam eder. Bu özellik, tek bir koşuda birden fazla hatanın toplanmasını sağlayarak teşhisi kolaylaştırır.

Soru 3: #expect ve #require arasındaki fark nedir?

Temel fark, başarısızlık sonrası davranışla ilgilidir:

  • #expect: hatayı kaydeder ve yürütmeye devam eder
  • #require: hatayı kaydeder ve testi anında durdurur

#require her zaman try ile birlikte kullanılmalıdır çünkü hata fırlatabilir.

ExpectVsRequire.swiftswift
import Testing

struct ApiResponse {
    let data: Data?
    let items: [Item]?
}

@Test func demonstrateExpectContinues() {
    let values = [1, 2, 3]

    // First #expect fails but test continues
    #expect(values.count == 10)  // ❌ Failure recorded

    // These verifications still execute
    #expect(values.first == 1)   // ✅ Success
    #expect(values.last == 3)    // ✅ Success

    // Result: 1 failure, 2 successes in the same test
}

@Test func demonstrateRequireStops() throws {
    let response = ApiResponse(data: nil, items: nil)

    // #require stops immediately if condition fails
    let data = try #require(response.data)  // ❌ Failure and STOP

    // This code NEVER executes if data is nil
    let json = try JSONDecoder().decode(User.self, from: data)
    #expect(json.name == "Alice")
}

Pratikte #require, optional'ların güvenli unwrap edilmesinde XCTUnwrap'in yerini mükemmel şekilde alır.

Altın Kural

Sonraki adımlar sonuca bağımlıysa (unwrap, ön koşullar) #require kullanın. Testin geri kalanını engellemeden başarısız olabilen bağımsız doğrulamalar için #expect kullanın.

#require ile İleri Seviye Desenler

Soru 4: Optional unwrapping için #require nasıl kullanılır?

#require optional unwrapping'de parlar. Varsa optional olmayan değeri döndürür, nil ise testi anında başarısız kılar.

RequireUnwrapping.swiftswift
import Testing

struct UserProfile {
    let id: Int
    let name: String
    let settings: Settings?
}

struct Settings {
    let theme: String
    let notifications: Bool
}

@Test func unwrapOptionalChain() throws {
    let profile = UserProfile(
        id: 1,
        name: "Alice",
        settings: Settings(theme: "dark", notifications: true)
    )

    // Safe unwrap - stops if nil
    let settings = try #require(profile.settings)

    // Now settings is no longer optional
    #expect(settings.theme == "dark")
    #expect(settings.notifications == true)
}

@Test func unwrapArrayElement() throws {
    let users = ["Alice", "Bob", "Charlie"]

    // Unwrap first element
    let firstUser = try #require(users.first)
    #expect(firstUser == "Alice")

    // Unwrap with safe index
    let secondUser = try #require(users[safe: 1])
    #expect(secondUser == "Bob")
}

@Test func unwrapDictionaryValue() throws {
    let config: [String: Any] = [
        "apiUrl": "https://api.example.com",
        "timeout": 30,
        "retryCount": 3
    ]

    // Unwrap and cast in a single operation
    let apiUrl = try #require(config["apiUrl"] as? String)
    let timeout = try #require(config["timeout"] as? Int)

    #expect(apiUrl.contains("https"))
    #expect(timeout > 0)
}

Bu yaklaşım guard let piramitlerini ortadan kaldırır ve test kodunu doğrusal ve okunabilir hale getirir.

Soru 5: Bir fonksiyonun hata fırlattığı nasıl test edilir?

Swift Testing, bir fonksiyonun belirli bir hata fırlattığını doğrulamak için #expect(throws:) sunar.

ErrorTesting.swiftswift
import Testing

enum ValidationError: Error, Equatable {
    case emptyName
    case invalidEmail
    case underAge(minimum: Int)
}

struct Validator {
    static func validateUser(name: String, email: String, age: Int) throws {
        guard !name.isEmpty else {
            throw ValidationError.emptyName
        }
        guard email.contains("@") else {
            throw ValidationError.invalidEmail
        }
        guard age >= 18 else {
            throw ValidationError.underAge(minimum: 18)
        }
    }
}

@Test func testThrowsSpecificError() {
    // Verify a specific error is thrown
    #expect(throws: ValidationError.emptyName) {
        try Validator.validateUser(name: "", email: "test@mail.com", age: 25)
    }

    #expect(throws: ValidationError.invalidEmail) {
        try Validator.validateUser(name: "Alice", email: "invalid", age: 25)
    }
}

@Test func testThrowsErrorType() {
    // Verify error type without specific value
    #expect(throws: ValidationError.self) {
        try Validator.validateUser(name: "Bob", email: "bob@mail.com", age: 15)
    }
}

@Test func testThrowsWithInspection() throws {
    // Capture error for detailed inspection
    let error = try #require(
        throws: ValidationError.self
    ) {
        try Validator.validateUser(name: "Charlie", email: "charlie@mail.com", age: 16)
    }

    // Verify error details
    if case .underAge(let minimum) = error {
        #expect(minimum == 18)
    }
}

Bu sözdizimi XCTAssertThrowsError'u daha açık ve tip güvenli bir API ile değiştirir.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

@Test ve @Suite ile Test Düzenlemesi

Soru 6: @Suite ile testler nasıl düzenlenir?

@Suite ilgili testleri mantıksal olarak gruplar. XCTestCase'in aksine sınıf kalıtımı gerektirmez.

TestSuiteOrganization.swiftswift
import Testing

// Suite for authentication tests
@Suite("Authentication Tests")
struct AuthenticationTests {

    // Shared property for all tests in the suite
    let authService = AuthService()

    @Test("Login with valid credentials succeeds")
    func loginWithValidCredentials() async throws {
        let result = try await authService.login(
            email: "user@example.com",
            password: "validPass123"
        )

        #expect(result.isSuccess)
        let token = try #require(result.token)
        #expect(!token.isEmpty)
    }

    @Test("Login with invalid password fails")
    func loginWithInvalidPassword() async {
        let result = await authService.login(
            email: "user@example.com",
            password: "wrong"
        )

        #expect(!result.isSuccess)
        #expect(result.error == .invalidCredentials)
    }
}

// Nested suites for hierarchical organization
@Suite("User Management")
struct UserManagementTests {

    @Suite("Creation")
    struct CreationTests {
        @Test func createUserWithValidData() {
            // Creation test
        }

        @Test func createUserWithDuplicateEmail() {
            // Duplicate error test
        }
    }

    @Suite("Deletion")
    struct DeletionTests {
        @Test func deleteExistingUser() {
            // Deletion test
        }

        @Test func deleteNonExistentUser() {
            // Error test
        }
    }
}

Suites, test alt kümelerinin çalıştırılmasına ve raporların okunabilir biçimde düzenlenmesine olanak tanır.

Soru 7: Testleri yapılandırmak için trait'ler nasıl kullanılır?

Trait'ler test davranışını değiştirir: çalıştırma koşulları, etiketler, timeout'lar vb.

TestTraits.swiftswift
import Testing

@Suite("API Integration Tests")
struct APITests {

    // Temporarily disabled test
    @Test(.disabled("Backend under maintenance"))
    func fetchUserProfile() async {
        // Does not execute
    }

    // Conditional test based on platform
    @Test
    @available(iOS 17, *)
    func useNewAPIFeature() {
        // Executes only on iOS 17+
    }

    // Test with tags for filtering
    @Test(.tags(.critical, .network))
    func criticalNetworkOperation() async throws {
        // Tagged test for filtering
    }

    // Test with custom timeout
    @Test(.timeLimit(.minutes(2)))
    func longRunningOperation() async {
        // Must complete within 2 minutes
    }

    // Trait combination
    @Test(
        "Complex data sync",
        .tags(.slow),
        .timeLimit(.minutes(5)),
        .bug("JIRA-1234", "Flaky on CI")
    )
    func complexDataSync() async throws {
        // Test documented with known bug
    }
}

// Custom tag definitions
extension Tag {
    @Tag static var critical: Self
    @Tag static var network: Self
    @Tag static var slow: Self
}

Trait'ler testleri kendini belgeler hale getirir ve komut satırı üzerinden seçici çalıştırmayı mümkün kılar.

Parametrize Edilmiş Testler

Soru 8: Parametrize edilmiş testler nasıl oluşturulur?

Swift Testing aynı testin farklı girdilerle parametreler aracılığıyla çalıştırılmasına olanak tanır.

ParameterizedTests.swiftswift
import Testing

struct EmailValidator {
    static func isValid(_ email: String) -> Bool {
        let pattern = #"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"#
        return email.range(of: pattern, options: .regularExpression) != nil
    }
}

// Parameterized test with a collection
@Test(arguments: [
    "user@example.com",
    "test.name@domain.org",
    "contact@company.co.uk"
])
func validEmailsAreAccepted(email: String) {
    #expect(EmailValidator.isValid(email))
}

@Test(arguments: [
    "invalid",
    "@nodomain.com",
    "no@tld",
    "spaces in@email.com"
])
func invalidEmailsAreRejected(email: String) {
    #expect(!EmailValidator.isValid(email))
}

// Test with tuples for input/output cases
@Test(arguments: [
    ("hello", "HELLO"),
    ("World", "WORLD"),
    ("Swift", "SWIFT")
])
func uppercaseConversion(input: String, expected: String) {
    #expect(input.uppercased() == expected)
}

// Test with Cartesian product of two collections
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinationTest(number: Int, letter: String) {
    // Runs for (1,"a"), (1,"b"), (2,"a"), (2,"b"), (3,"a"), (3,"b")
    let combined = "\(number)\(letter)"
    #expect(combined.count == 2)
}

Her parametre kombinasyonu bağımsız bir test üretir ve başarısız vakaların belirlenmesini kolaylaştırır.

Soru 9: Asenkron kod nasıl test edilir?

Swift Testing, async/await'i yerel olarak entegre ederek asenkron testleri büyük ölçüde basitleştirir.

AsyncTesting.swiftswift
import Testing

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }

    func clear() {
        items.removeAll()
    }
}

@Suite("Async Operations")
struct AsyncTests {

    @Test func basicAsyncOperation() async {
        // No need for expectation or wait
        let result = await fetchData()
        #expect(!result.isEmpty)
    }

    @Test func asyncWithTimeout() async throws {
        // Use Task.sleep to simulate delay
        try await Task.sleep(for: .milliseconds(100))

        let data = await loadConfiguration()
        let config = try #require(data)
        #expect(config.isValid)
    }

    @Test func testActorIsolation() async {
        let store = DataStore()

        // Operations on actor sequentially
        await store.add("Item 1")
        await store.add("Item 2")

        let items = await store.getAll()
        #expect(items.count == 2)
        #expect(items.contains("Item 1"))
    }

    @Test func testConcurrentOperations() async {
        let store = DataStore()

        // Concurrent execution with TaskGroup
        await withTaskGroup(of: Void.self) { group in
            for i in 1...10 {
                group.addTask {
                    await store.add("Item \(i)")
                }
            }
        }

        let items = await store.getAll()
        #expect(items.count == 10)
    }
}

// Async helpers for tests
func fetchData() async -> [String] {
    try? await Task.sleep(for: .milliseconds(50))
    return ["data1", "data2"]
}

func loadConfiguration() async -> Configuration? {
    Configuration(isValid: true)
}

struct Configuration {
    let isValid: Bool
}
Varsayılan Paralellik

Swift Testing testleri varsayılan olarak paralel çalıştırır. Paylaşılan durumu değiştiren testler için suite üzerinde .serialized kullanın veya durumu actor'lerle izole edin.

XCTest'ten Swift Testing'e Geçiş

Soru 10: XCTest'ten kademeli olarak nasıl geçilir?

İki framework aynı projede bir arada bulunabilir. Kademeli bir geçiş tavsiye edilir.

MigrationStrategy.swiftswift
import XCTest
import Testing

// ⚠️ CRITICAL RULE: Never mix frameworks in the same test

// ❌ INCORRECT - Mixing forbidden
class BadMixedTest: XCTestCase {
    func testMixed() {
        #expect(true)  // Does not work in XCTestCase
    }
}

// ✅ CORRECT - Pure XCTest for existing tests
class LegacyUserTests: XCTestCase {
    func testUserCreation() {
        let user = User(name: "Test")
        XCTAssertNotNil(user)
        XCTAssertEqual(user.name, "Test")
    }
}

// ✅ CORRECT - Swift Testing for new tests
@Suite("User Tests - Modern")
struct ModernUserTests {
    @Test func userCreation() {
        let user = User(name: "Test")
        #expect(user.name == "Test")
    }
}

// Migration strategy by file
// 1. Identify tests to migrate (start with simplest)
// 2. Create new file with @Suite
// 3. Rewrite tests one by one
// 4. Delete old XCTest file once validated

| XCTest | Swift Testing | |--------|---------------| | XCTAssertTrue(x) | #expect(x) | | XCTAssertFalse(x) | #expect(!x) | | XCTAssertEqual(a, b) | #expect(a == b) | | XCTAssertNil(x) | #expect(x == nil) | | XCTAssertNotNil(x) | #expect(x != nil) | | XCTUnwrap(x) | try #require(x) | | XCTAssertThrowsError | #expect(throws:) |

Soru 11: XCTest'in hangi özellikleri henüz Swift Testing'de yok?

Swift Testing (Swift 6) henüz XCTest'in tüm kullanım senaryolarını kapsamaz.

MissingFeatures.swiftswift
// ❌ NOT SUPPORTED: Performance tests
// Stick with XCTest for measuring performance
class PerformanceTests: XCTestCase {
    func testPerformance() {
        measure {
            // Code to measure
            _ = (0..<1000).map { $0 * 2 }
        }
    }
}

// ❌ NOT SUPPORTED: UI tests (XCUITest)
// Continue using XCUITest for interface tests
class UITests: XCTestCase {
    func testLoginFlow() {
        let app = XCUIApplication()
        app.launch()
        // UI tests...
    }
}

// ✅ SUPPORTED: Async integration tests
@Test func integrationTest() async throws {
    let api = APIClient()
    let response = try await api.fetchUsers()
    #expect(!response.isEmpty)
}

// ✅ SUPPORTED: Mocking with protocols
@Test func mockingWithProtocols() async {
    let mockService = MockUserService()
    let viewModel = UserViewModel(service: mockService)

    await viewModel.loadUser(id: 1)

    #expect(viewModel.user?.name == "Mock User")
}

UI veya performans testleri içeren projelerde bu özel durumlar için XCTest'i kullanmaya devam edin.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Aldatıcı Mülakat Soruları

Soru 12: Neden #require try gerektirir ama #expect gerektirmez?

#require koşul başarısız olursa testi kesmek zorunda olduğundan hata fırlatabilir. #expect ise yalnızca hatayı kaydeder ve devam eder, dolayısıyla bir şey fırlatmaz.

RequireTryExplanation.swiftswift
import Testing

@Test func explainTryRequirement() throws {
    let optionalValue: String? = nil

    // #expect returns Void - no error thrown
    // Test continues even if it fails
    #expect(optionalValue != nil)  // No try

    // #require can throw ExpectationFailedError
    // Test stops if it fails
    // Must be marked with try
    let value = try #require(optionalValue)  // Requires try

    // If we reach here, optionalValue was not nil
    #expect(!value.isEmpty)
}

// The internal signature resembles:
// func #expect(_ condition: Bool) -> Void
// func #require<T>(_ value: T?) throws -> T

Bu mimari ayrım, derleyicinin doğru hata yönetimini garanti etmesini sağlar.

Soru 13: Swift Testing paralelliği nasıl yönetir?

Varsayılan olarak tüm testler paralel çalışır; bu suites'i hızlandırır ancak doğru durum izolasyonu gerektirir.

ParallelismHandling.swiftswift
import Testing

// ❌ PROBLEM: Mutable shared state
var sharedCounter = 0  // Dangerous in parallel!

@Suite("Problematic Parallel Tests")
struct ProblematicTests {
    @Test func incrementCounter1() {
        sharedCounter += 1  // Race condition!
    }

    @Test func incrementCounter2() {
        sharedCounter += 1  // Race condition!
    }
}

// ✅ SOLUTION 1: Force sequential execution
@Suite("Sequential Tests", .serialized)
struct SequentialTests {
    static var counter = 0

    @Test func first() {
        Self.counter += 1
        #expect(Self.counter == 1)
    }

    @Test func second() {
        Self.counter += 1
        #expect(Self.counter == 2)
    }
}

// ✅ SOLUTION 2: Isolate state per test
@Suite("Isolated Tests")
struct IsolatedTests {
    @Test func independentTest1() {
        var localCounter = 0
        localCounter += 1
        #expect(localCounter == 1)
    }

    @Test func independentTest2() {
        var localCounter = 0
        localCounter += 1
        #expect(localCounter == 1)
    }
}

// ✅ SOLUTION 3: Use actor for shared state
actor TestState {
    var value = 0

    func increment() -> Int {
        value += 1
        return value
    }
}

@Suite("Actor-based Tests")
struct ActorTests {
    let state = TestState()

    @Test func safeIncrement() async {
        let result = await state.increment()
        #expect(result > 0)
    }
}

.serialized trait'i, tüm bir suite'in sıralı yürütülmesini garanti eder.

Soru 14: Callback'ler için confirmation nasıl kullanılır?

Callback'li (async olmayan) API'ler için Swift Testing confirmation sunar.

ConfirmationPattern.swiftswift
import Testing

// Legacy service with callback
class LegacyService {
    func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
            completion(.success("Data loaded"))
        }
    }
}

@Test func testCallbackWithConfirmation() async {
    let service = LegacyService()

    // confirmation waits for it to be called
    await confirmation("Data callback received") { confirm in
        service.fetchData { result in
            if case .success(let data) = result {
                #expect(data == "Data loaded")
                confirm()  // Signals callback was executed
            }
        }
    }
}

// For callbacks called multiple times
@Test func testMultipleCallbacks() async {
    let publisher = EventPublisher()

    // expectedCount specifies expected call count
    await confirmation("Events received", expectedCount: 3) { confirm in
        publisher.onEvent = { event in
            #expect(!event.isEmpty)
            confirm()  // Called 3 times
        }

        publisher.emit("Event 1")
        publisher.emit("Event 2")
        publisher.emit("Event 3")
    }
}

class EventPublisher {
    var onEvent: ((String) -> Void)?

    func emit(_ event: String) {
        DispatchQueue.global().async {
            self.onEvent?(event)
        }
    }
}

confirmation, XCTestExpectation ve wait(for:timeout:)'un yerini zarif bir şekilde alır.

Sonuç

Swift Testing, Apple platformlarında testlerin geleceğini temsil eder. İki makro #expect ve #require, hata mesajı kalitesini iyileştirirken test yazımını da büyük ölçüde basitleştirir.

Mülakatlar için akılda tutulması gereken kilit noktalar:

  • #expect başarısızlık sonrası devam eder, #require anında durur
  • #require hata fırlatabildiği için try gerektirir
  • ✅ Swift Testing varsayılan olarak paralel çalışır
  • ✅ İki framework bir arada bulunabilir ancak aynı testte karıştırılmamalıdır
  • ✅ XCTest, UI ve performans testleri için hâlâ gereklidir
  • ✅ Trait'ler test davranışının ince ayarlı yapılandırılmasına olanak tanır
  • ✅ Parametrize edilmiş testler kod tekrarını önler

Swift Testing'e geçiş, en basit testlerden başlayarak dosya dosya kademeli olarak yapılabilir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#swift
#ios
#testing
#xctest
#interview

Paylaş

İlgili makaleler