# Swift Testing Framework 면접 2026: #expect와 #require 매크로 vs XCTest > iOS 면접을 위한 새로운 Swift Testing Framework를 마스터합니다: #expect와 #require 매크로, XCTest 마이그레이션, 고급 패턴 및 흔한 함정. - Published: 2026-03-05 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swift, ios, testing, xctest, interview - Reading time: 14 min --- WWDC 2024에서 발표되고 Swift 6 및 Xcode 16과 함께 출시된 Swift Testing은 Swift에서 테스트가 작동하는 방식을 완전히 재구상한 것입니다. 이 프레임워크는 XCTest의 40개가 넘는 어서션을 단 두 개의 매크로 `#expect`와 `#require`로 대체합니다. 면접관들은 이제 iOS 기술 면접에서 이 지식을 정기적으로 평가합니다. > **가이드 형식** > > 각 섹션은 자세한 답변과 작동하는 코드와 함께 기술 면접 질문을 반영합니다. 진행은 기본 개념에서 고급 패턴으로 이동합니다. ## Swift Testing 기초 ### 질문 1: Swift Testing과 XCTest의 주요 차이점은 무엇입니까? Swift Testing은 XCTest와 비교하여 다섯 가지 근본적인 변화를 가져옵니다: 1. **선언적 구문**: `test` 접두사 대신 `@Test` 속성 2. **두 개의 범용 매크로**: `#expect`와 `#require`가 40개 이상의 어서션을 대체합니다 3. **기본 병렬 실행**: 모든 테스트가 동시에 실행됩니다 4. **네이티브 async 지원**: Swift Concurrency와의 완전한 통합 5. **크로스 플랫폼**: Apple 플랫폼, Linux 및 Windows에서 작동합니다 ```swift // TestComparison.swift 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) } ``` 핵심적인 차이는 표현력에 있습니다: Swift Testing은 특수화된 어서션 대신 표준 Swift 표현식을 사용하여 테스트를 더 읽기 쉽게 만들고 오류 메시지를 더 풍부하게 만듭니다. ### 질문 2: #expect 매크로는 어떻게 작동합니까? `#expect` 매크로는 부울 표현식이 참인지 검증합니다. 자세한 오류 메시지를 제공하기 위해 평가된 값을 자동으로 캡처합니다. `XCTAssert`와 달리 네이티브 Swift 구문을 사용합니다. ```swift // ExpectMacroBasics.swift 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)" ) } ``` `#expect`가 실패해도 테스트는 계속 실행됩니다. 이 특성은 한 번의 실행에서 여러 실패를 수집할 수 있게 하여 진단을 용이하게 만듭니다. ### 질문 3: #expect와 #require의 차이점은 무엇입니까? 근본적인 차이는 실패 후 동작에 관한 것입니다: - `#expect`: 실패를 기록하고 실행을 **계속**합니다 - `#require`: 실패를 기록하고 테스트를 **즉시 중지**합니다 `#require`는 오류를 던질 수 있기 때문에 항상 `try`와 함께 사용해야 합니다. ```swift // ExpectVsRequire.swift 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") } ``` 실제로 `#require`는 옵셔널의 안전한 언래핑을 위해 `XCTUnwrap`을 완벽하게 대체합니다. > **황금률** > > 후속 단계가 결과에 의존할 때 (언래핑, 전제 조건) `#require`를 사용하십시오. 테스트의 나머지 부분을 차단하지 않고 실패할 수 있는 독립적인 검증에는 `#expect`를 사용하십시오. ## #require를 사용한 고급 패턴 ### 질문 4: 옵셔널 언래핑에 #require를 어떻게 사용합니까? `#require`는 옵셔널 언래핑에서 빛을 발합니다. 존재하면 옵셔널이 아닌 값을 반환하고, `nil`이면 즉시 테스트를 실패시킵니다. ```swift // RequireUnwrapping.swift 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) } ``` 이 접근 방식은 `guard let` 피라미드를 제거하고 테스트 코드를 선형적이고 읽기 쉽게 만듭니다. ### 질문 5: 함수가 오류를 던지는 것을 어떻게 테스트합니까? Swift Testing은 함수가 특정 오류를 던지는 것을 검증하기 위해 `#expect(throws:)`를 제공합니다. ```swift // ErrorTesting.swift 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) } } ``` 이 구문은 `XCTAssertThrowsError`를 더 명확하고 타입 안전한 API로 대체합니다. ## @Test와 @Suite를 사용한 테스트 구성 ### 질문 6: @Suite로 테스트를 어떻게 구성합니까? `@Suite`는 관련 테스트를 논리적으로 그룹화합니다. `XCTestCase`와 달리 클래스 상속을 요구하지 않습니다. ```swift // TestSuiteOrganization.swift 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 } } } ``` 스위트는 테스트 하위 집합을 실행하고 보고서를 읽기 쉬운 방식으로 구성할 수 있게 합니다. ### 질문 7: 트레이트를 사용해 테스트를 어떻게 구성합니까? 트레이트는 테스트 동작을 수정합니다: 실행 조건, 태그, 타임아웃 등. ```swift // TestTraits.swift 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 } ``` 트레이트는 테스트를 자체 문서화하고 명령줄을 통한 선택적 실행을 가능하게 합니다. ## 매개변수화된 테스트 ### 질문 8: 매개변수화된 테스트를 어떻게 만듭니까? Swift Testing은 매개변수를 통해 동일한 테스트를 다른 입력으로 실행할 수 있게 합니다. ```swift // ParameterizedTests.swift 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) } ``` 각 매개변수 조합은 독립된 테스트를 생성하여 실패하는 케이스를 쉽게 식별할 수 있게 합니다. ### 질문 9: 비동기 코드를 어떻게 테스트합니까? Swift Testing은 async/await를 네이티브로 통합하여 비동기 테스트를 극적으로 간소화합니다. ```swift // AsyncTesting.swift 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 } ``` > **기본 병렬성** > > Swift Testing은 기본적으로 테스트를 병렬로 실행합니다. 공유 상태를 수정하는 테스트의 경우 스위트에 `.serialized`를 사용하거나 액터로 상태를 격리하십시오. ## XCTest에서 Swift Testing으로 마이그레이션 ### 질문 10: XCTest에서 점진적으로 어떻게 마이그레이션합니까? 두 프레임워크는 동일한 프로젝트에서 공존합니다. 점진적인 마이그레이션이 권장됩니다. ```swift // MigrationStrategy.swift 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:)` | ### 질문 11: XCTest의 어떤 기능이 아직 Swift Testing에 없습니까? Swift Testing(Swift 6)은 아직 XCTest의 모든 사용 사례를 다루지 않습니다. ```swift // MissingFeatures.swift // ❌ 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 또는 성능 테스트가 있는 프로젝트의 경우, 이러한 특정 케이스에는 XCTest를 유지하십시오. ## 까다로운 면접 질문 ### 질문 12: 왜 #require는 try가 필요하지만 #expect는 필요하지 않습니까? `#require`는 조건이 실패하면 테스트를 중단해야 하므로 오류를 던질 수 있습니다. `#expect`는 실패를 기록하고 계속 진행하기만 하므로 아무것도 던지지 않습니다. ```swift // RequireTryExplanation.swift 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(_ value: T?) throws -> T ``` 이 아키텍처적 구분은 컴파일러가 올바른 오류 처리를 보장할 수 있게 합니다. ### 질문 13: Swift Testing은 병렬성을 어떻게 관리합니까? 기본적으로 모든 테스트는 병렬로 실행되어 스위트를 가속화하지만 적절한 상태 격리가 필요합니다. ```swift // ParallelismHandling.swift 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` 트레이트는 전체 스위트의 순차 실행을 보장합니다. ### 질문 14: 콜백에 confirmation을 어떻게 사용합니까? 콜백이 있는 (비async) API의 경우, Swift Testing은 `confirmation`을 제공합니다. ```swift // ConfirmationPattern.swift import Testing // Legacy service with callback class LegacyService { func fetchData(completion: @escaping (Result) -> 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`과 `wait(for:timeout:)`를 우아하게 대체합니다. ## 결론 Swift Testing은 Apple 플랫폼에서 테스트의 미래를 대표합니다. 두 매크로 `#expect`와 `#require`는 테스트 작성을 극적으로 간소화하면서 동시에 오류 메시지의 품질을 향상시킵니다. **면접에서 기억해야 할 핵심 사항:** - ✅ `#expect`는 실패 후에도 계속 진행하고, `#require`는 즉시 중지합니다 - ✅ `#require`는 오류를 던질 수 있기 때문에 `try`가 필요합니다 - ✅ Swift Testing은 기본적으로 병렬로 실행됩니다 - ✅ 두 프레임워크는 공존하지만 동일한 테스트에서 혼합되어서는 안 됩니다 - ✅ XCTest는 UI 및 성능 테스트에 여전히 필요합니다 - ✅ 트레이트는 테스트 동작을 세밀하게 구성할 수 있게 합니다 - ✅ 매개변수화된 테스트는 코드 중복을 피합니다 Swift Testing으로의 마이그레이션은 가장 단순한 테스트부터 시작하여 파일별로 점진적으로 수행할 수 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ios/swift-testing-framework-macros-expect-require-xctest