Swift Testing Framework 면접 2026: #expect와 #require 매크로 vs XCTest
iOS 면접을 위한 새로운 Swift Testing Framework를 마스터합니다: #expect와 #require 매크로, XCTest 마이그레이션, 고급 패턴 및 흔한 함정.

WWDC 2024에서 발표되고 Swift 6 및 Xcode 16과 함께 출시된 Swift Testing은 Swift에서 테스트가 작동하는 방식을 완전히 재구상한 것입니다. 이 프레임워크는 XCTest의 40개가 넘는 어서션을 단 두 개의 매크로 #expect와 #require로 대체합니다. 면접관들은 이제 iOS 기술 면접에서 이 지식을 정기적으로 평가합니다.
각 섹션은 자세한 답변과 작동하는 코드와 함께 기술 면접 질문을 반영합니다. 진행은 기본 개념에서 고급 패턴으로 이동합니다.
Swift Testing 기초
질문 1: Swift Testing과 XCTest의 주요 차이점은 무엇입니까?
Swift Testing은 XCTest와 비교하여 다섯 가지 근본적인 변화를 가져옵니다:
- 선언적 구문:
test접두사 대신@Test속성 - 두 개의 범용 매크로:
#expect와#require가 40개 이상의 어서션을 대체합니다 - 기본 병렬 실행: 모든 테스트가 동시에 실행됩니다
- 네이티브 async 지원: Swift Concurrency와의 완전한 통합
- 크로스 플랫폼: Apple 플랫폼, Linux 및 Windows에서 작동합니다
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 구문을 사용합니다.
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와 함께 사용해야 합니다.
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이면 즉시 테스트를 실패시킵니다.
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:)를 제공합니다.
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로 대체합니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
@Test와 @Suite를 사용한 테스트 구성
질문 6: @Suite로 테스트를 어떻게 구성합니까?
@Suite는 관련 테스트를 논리적으로 그룹화합니다. XCTestCase와 달리 클래스 상속을 요구하지 않습니다.
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: 트레이트를 사용해 테스트를 어떻게 구성합니까?
트레이트는 테스트 동작을 수정합니다: 실행 조건, 태그, 타임아웃 등.
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은 매개변수를 통해 동일한 테스트를 다른 입력으로 실행할 수 있게 합니다.
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를 네이티브로 통합하여 비동기 테스트를 극적으로 간소화합니다.
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에서 점진적으로 어떻게 마이그레이션합니까?
두 프레임워크는 동일한 프로젝트에서 공존합니다. 점진적인 마이그레이션이 권장됩니다.
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의 모든 사용 사례를 다루지 않습니다.
// ❌ 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를 유지하십시오.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
까다로운 면접 질문
질문 12: 왜 #require는 try가 필요하지만 #expect는 필요하지 않습니까?
#require는 조건이 실패하면 테스트를 중단해야 하므로 오류를 던질 수 있습니다. #expect는 실패를 기록하고 계속 진행하기만 하므로 아무것도 던지지 않습니다.
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이 아키텍처적 구분은 컴파일러가 올바른 오류 처리를 보장할 수 있게 합니다.
질문 13: Swift Testing은 병렬성을 어떻게 관리합니까?
기본적으로 모든 테스트는 병렬로 실행되어 스위트를 가속화하지만 적절한 상태 격리가 필요합니다.
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을 제공합니다.
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과 wait(for:timeout:)를 우아하게 대체합니다.
결론
Swift Testing은 Apple 플랫폼에서 테스트의 미래를 대표합니다. 두 매크로 #expect와 #require는 테스트 작성을 극적으로 간소화하면서 동시에 오류 메시지의 품질을 향상시킵니다.
면접에서 기억해야 할 핵심 사항:
- ✅
#expect는 실패 후에도 계속 진행하고,#require는 즉시 중지합니다 - ✅
#require는 오류를 던질 수 있기 때문에try가 필요합니다 - ✅ Swift Testing은 기본적으로 병렬로 실행됩니다
- ✅ 두 프레임워크는 공존하지만 동일한 테스트에서 혼합되어서는 안 됩니다
- ✅ XCTest는 UI 및 성능 테스트에 여전히 필요합니다
- ✅ 트레이트는 테스트 동작을 세밀하게 구성할 수 있게 합니다
- ✅ 매개변수화된 테스트는 코드 중복을 피합니다
Swift Testing으로의 마이그레이션은 가장 단순한 테스트부터 시작하여 파일별로 점진적으로 수행할 수 있습니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

StoreKit 2 인터뷰: 구독 관리 및 영수증 검증
StoreKit 2, 구독 관리, 영수증 검증, 인앱 구매 구현에 관한 iOS 인터뷰 질문을 실용적인 Swift 코드 예제와 함께 마스터하십시오.

iOS 푸시 알림 면접 2026: APNs, 토큰, 트러블슈팅
Push Notifications, APNs, 토큰 관리, 트러블슈팅에 관한 iOS 면접 준비 완벽 가이드입니다. 자주 묻는 질문에 상세한 답변을 함께 담았습니다.

iOS 개발자를 위한 Swift 면접 질문 Top 25
iOS 면접에서 가장 자주 출제되는 Swift 질문 25개를 코드 예제와 함께 완벽 정리. 옵셔널, 클로저, ARC, 프로토콜, async/await 및 고급 패턴까지 체계적으로 다룹니다.