Swift Testing: XCTest를 대체하는 프레임워크, 그리고 XCTest에 남는 것
2013년부터 Apple의 테스트 프레임워크였던 XCTest가 Swift Testing으로 대체되고 있습니다. Apple은 아직 “deprecated”라고 말하지 않습니다(XCTest는 여전히 제공되고, 작동하며, UI 자동화와 성능 테스트를 호스팅합니다). 그러나 WWDC 2024 이후 모든 Apple 세션, 모든 Swift Forum 스레드, 모든 새로운 샘플 프로젝트는 Swift 코드를 Swift Testing으로 안내합니다. 이 프레임워크는 Xcode 16+ 및 Swift 6와 함께 제공되며1, macOS, iOS, watchOS, tvOS, visionOS, Linux에서 실행되고2, 다른 멘탈 모델을 제시합니다. 테스트는 함수이고, 스위트는 타입이며, 구성은 트레이트이고, 병렬 실행이 기본값입니다.
마이그레이션은 점진적입니다. Apple의 공식 문서는 두 프레임워크를 동일한 타깃에서 나란히 실행하는 것을 명시적으로 지원합니다3. 이는 사소하지 않은 코드베이스에 대한 올바른 선택이 “전부 다시 작성하기”가 아니라 “새 테스트는 Swift Testing으로 작성하고 오래된 것은 손댈 때마다 마이그레이션하기”임을 의미합니다. 이 글은 API 표면을 살펴보고, 마이그레이션 경계(XCTest에 남는 것)를 명명하며, XCTest의 클래스 계층 구성을 대체하는 트레이트 어휘를 다룹니다.
TL;DR
- Swift Testing은 XCTest의 클래스 기반, 문자열 타입 모델을 매크로로 대체합니다:
@Test,@Suite,#expect(...),#require(...)4. - 테스트는 자유 함수 또는 타입의 메서드입니다. 스위트는 테스트 함수를 포함하는 모든 타입입니다(
@Suite는 표시 이름이나 트레이트를 지정할 때만 필요합니다). - 구성은 클래스 계층과 명명 규칙에서 트레이트로 이동합니다:
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)5. @Test(arguments:)를 통한 파라미터화된 테스트는 하나의 부모 테스트 결과와 인수당 하나의 자식 결과를 생성합니다. 기본 병렬 실행입니다6.- 마이그레이션은 빅뱅이 아니라 나란히 진행됩니다. UI 자동화(
XCUIApplication)와 성능 테스트(XCTMetric)는 Swift Testing이 다루지 않으므로 XCTest에 남습니다3. - WWDC 2025에서는 커스텀 첨부 파일(테스트 아티팩트)과 종료 테스트(특정 조건에서 종료될 것으로 예상되는 코드 검증)가 추가되었습니다7.
Swift Testing이 존재하는 이유
XCTest의 고통점은 대규모 iOS 테스트 스위트를 유지 관리한 사람이라면 누구나 잘 알고 있습니다.
문자열 타입 단언. XCTAssertEqual(a, b, "message")는 원본 Swift 표현식을 캡처하지 않습니다. 단언이 실패하면 실패 메시지는 런타임 값을 알려주지만 어떤 표현식이 그것을 만들었는지는 알려주지 않습니다. 디버깅에는 테스트 소스를 읽고 컨텍스트를 재구성해야 합니다.
클래스 기반 구성. 설정, 해제, 공유 상태에는 XCTestCase 서브클래싱이 필요합니다. 테스트 비활성화에는 이름을 변경하거나(검색 패턴이 놓치도록) #if DEBUG로 감싸야 합니다. 어떤 테스트를 실행할지 선택하려면 신중한 명명이 필요합니다.
동시성 불일치. XCTest는 Swift Concurrency보다 앞서 등장했습니다. XCTestExpectation 더하기 wait(for:timeout:)는 비동기 코드로 가는 레거시 브리지입니다. 패턴은 장황하고 오류가 발생하기 쉽습니다. 모던 Swift 코드는 async/await를 사용합니다. XCTest의 관용구는 시대착오적으로 느껴집니다.
약한 파라미터화. XCTest에는 일급 파라미터화된 테스트 지원이 없습니다. 개발자는 테스트 메서드 내부의 for 루프(여러 케이스에 대해 하나의 테스트 결과를 생성함)나 코드 생성기(템플릿에서 N개의 테스트 메서드를 생성함)로 흉내 냅니다.
Swift Testing은 각각을 다룹니다.
import Testing
@Test func userInitializesWithDefaults() {
let user = User()
#expect(user.name == "Anonymous")
#expect(user.preferences.isEmpty)
}
#expect는 전체 표현식을 캡처합니다. 단언이 실패하면 테스트 출력은 user.name == "Anonymous"가 실패한 이유가 user.name이 "Guest"였기 때문임을 보여줍니다. 매크로가 구문 트리를 보기 때문에 프레임워크가 표현식을 읽는 작업을 합니다. “예상 X, 실제 Y” 추측은 없습니다.
매크로
네 개의 매크로가 일을 합니다.
@Test와 @Suite
@Test는 함수를 테스트로 표시합니다. 이 함수는 파일 스코프, 구조체 내부, 액터 내부, 열거형 내부 등 Swift에서 함수가 살 수 있는 어디에서나 살 수 있습니다. 프레임워크는 @Test로 표시된 함수를 발견하고 실행합니다.
@Test func loginSucceedsWithValidCredentials() async throws {
let result = try await api.login(username: "alice", password: "valid")
#expect(result.isAuthenticated)
}
struct AuthenticationTests {
@Test func loginRejectsEmptyPassword() async throws {
let result = try await api.login(username: "alice", password: "")
#expect(!result.isAuthenticated)
#expect(result.error == .emptyPassword)
}
}
@Suite는 타입을 테스트 스위트로 명시적으로 표시합니다. 타입이 @Test 함수를 포함하면 표시는 암시적입니다. 명시적 @Suite는 커스텀 표시 이름을 추가하거나 스위트 수준에서 트레이트를 적용할 때만 필요합니다4.
@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
@Test func loginSucceeds() async throws { ... }
@Test func loginFails() async throws { ... }
}
#expect와 #require
#expect는 표준 단언입니다. 기대가 실패하더라도 테스트는 계속됩니다(실패는 기록되고, 후속 단언도 계속 실행됩니다). #require는 빠른 실패 변형입니다. 요구사항이 실패하면 테스트는 즉시 중단됩니다. 후속 단언에서 무의미한 오류를 발생시킬 수 있는 사전 조건에는 #require를 사용하세요.
@Test func userPreferencesLoad() async throws {
let user = try #require(await store.fetchUser(id: 42))
// fetchUser가 nil을 반환하면 테스트는 여기서 중단됩니다. 다음 줄은
// 언래핑되지 않은 옵셔널에 대해 절대 실행되지 않습니다.
#expect(user.preferences["theme"] == "dark")
#expect(user.preferences.count >= 1)
}
#require 앞의 try는 중요합니다. #require는 요구사항이 충족되면 언래핑된 값(이 경우 옵셔널이 아닌 User)을 반환하거나 그렇지 않으면 throw합니다. 언래핑을 위해 #require를 사용하려면 테스트 함수가 throws여야 합니다.
파라미터화된 테스트
@Test(arguments:)는 인수당 한 번씩 테스트 함수를 실행합니다. 프레임워크는 각 인수를 자식 테스트로, 부모 테스트를 롤업으로 보고합니다.
@Test(arguments: [
("alice", "password123", true),
("bob", "", false),
("", "password123", false),
("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
let result = try await api.login(username: username, password: password)
#expect(result.isAuthenticated == expectedSuccess)
}
프레임워크는 기본적으로 네 개의 인수를 병렬로 실행합니다. 테스트 실패 메시지는 어떤 인수 튜플이 실패했는지 식별하므로 파라미터화된 실패를 디버깅하기 위해 테스트를 분리해서 실행할 필요가 없습니다.
독립적인 컬렉션인 인수의 경우 @Test(arguments: arrayA, arrayB)는 교차곱(모든 조합)을 실행합니다. 순차 쌍의 경우 zip(arrayA, arrayB)를 사용하고 결과 시퀀스를 전달하세요.
트레이트: 일급 값으로서의 구성
XCTest는 클래스 계층, 명명 규칙, Xcode 테스트 플랜을 사용해 테스트를 구성합니다. Swift Testing은 트레이트를 사용합니다. @Test와 @Suite 선언에 적용되는 값입니다5.
@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }
@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }
@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }
@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }
@Suite(.serialized)
struct DatabaseTests {
// 이 스위트의 모든 테스트는 순차적으로 실행됩니다.
// 테스트가 데이터베이스 픽스처를 공유할 때 유용합니다.
@Test func insertUser() { ... }
@Test func updateUser() { ... }
@Test func deleteUser() { ... }
}
트레이트 어휘는 XCTest가 커스텀 인프라를 필요로 했던 경우들을 다룹니다.
.enabled(if:)와.disabled(_:). 런타임 표현식과 선택적 이유를 사용한 조건부 활성화.#if DEBUG블록과 이름 변경 트릭을 대체합니다..serialized. 스위트나 파라미터화된 테스트를 순차적으로 표시합니다. 상속 가능합니다. 직렬화된 스위트는 모든 자식 테스트를 직렬로 강제합니다6..timeLimit(...). 테스트당 시간 제한. 제한을 초과하면 테스트는 종료되고 실패로 보고됩니다..tags(...). 필터링용 태그. Xcode 테스트 플랜에서 포함/제외 태그 규칙을 구성합니다. 플랜은xcodebuild test -testPlan <name>을 통해 실행됩니다.-only-testing:은 태그가 아니라 테스트 식별자(타깃, 스위트, 함수)로 필터링한다는 점에 유의하세요..bug(...). 테스트를 이유와 함께 버그 ID에 연결합니다. 링크는 테스트 보고서에 표시되며 (실패가 알려진 테스트의 경우) 실패가 CI를 차단하는 것을 방지합니다.
커스텀 트레이트도 지원됩니다. 앱은 프로젝트별 필요(예: .requiresKeychain, .skipIfOffline)에 맞춰 자체 트레이트를 정의할 수 있습니다.
병렬 실행이 기본값
Swift Testing은 파라미터화된 인수를 포함해 기본적으로 테스트를 병렬로 실행합니다. 기본값이 중요한 이유는 대부분의 테스트 스위트는 시간이 지남에 따라 성장하고, 병렬 실행은 30초 테스트 실행과 5분 테스트 실행의 차이이기 때문입니다. XCTest는 명시적 옵트인이 필요했습니다(테스트 플랜의 테스트 병렬 실행 토글). Swift Testing은 기본값을 뒤집습니다.
함의: 테스트는 독립적이어야 합니다. 공유 상태(데이터베이스 픽스처, 모킹된 파일 시스템, 앱 수준 싱글톤)는 테스트당 격리되거나(각 테스트는 새로운 픽스처를 받음) .serialized로 표시되어야 합니다(스위트는 순차적으로 실행됩니다). 전역 상태를 변경하고 직렬 실행을 가정하는 테스트는 Swift Testing이 XCTest보다 더 빨리 드러내는 버그입니다.
마이그레이션: 빅뱅이 아니라 나란히
마이그레이션에 대한 Apple의 공식 입장은 점진적입니다3.
- 두 프레임워크는 동일한 테스트 타깃에 공존합니다.
- 새 테스트는 즉시 Swift Testing으로 작성할 수 있습니다.
- 기존 XCTest 테스트는 변경 없이 계속 작동합니다.
- XCTest 테스트는 편리할 때(손댄 파일, 새 기능, 리팩토링) Swift Testing으로 마이그레이션합니다.
두 가지 경우는 무기한으로 XCTest에 남습니다.
UI 자동화 테스트. XCUIApplication, XCUIElement, XCUIElementQuery, 전체 UI 테스팅 API는 XCTest 전용입니다3. Swift Testing은 대체를 제공하지 않습니다. UI 테스트는 Apple이 Swift Testing의 적용 범위를 확장할 때까지 XCTest에 남습니다.
성능 테스트. XCTMetric, measure(metrics:), 성능 베이스라인도 XCTest 전용입니다3. Swift Testing은 아직 동등한 것을 갖추고 있지 않습니다.
그 외 모든 것(단위 테스트, 통합 테스트, 비동기 테스트, 파라미터화된 테스트)에서 Swift Testing은 새 코드의 권장 기본값입니다.
WWDC 2025에서 추가된 것
WWDC 2025에서 Swift Testing에 두 가지 주목할 만한 추가 사항이 있었습니다7.
커스텀 첨부 파일. 테스트는 실패 분류를 위해 임의의 Attachable 데이터를 결과에 첨부할 수 있습니다. 실패한 스크린샷 이미지, 진단 로그, 생성된 입력 파일. 첨부 파일은 Xcode의 테스트 리포터와 CI 아티팩트에 표시됩니다. Sources/Testing/Attachments/Attachment.swift의 Attachment.record(_:named:sourceLocation:) API가 표면을 정의합니다. Attachable을 준수하는 값(Data, String, 옵트인 준수를 통한 이미지 타입)이 허용됩니다.
@Test func reportLayoutAtFailure() async throws {
let result = await renderer.run(LayoutTestCase.complex)
if !result.passes {
Attachment.record(result.diagnostics, named: "diagnostics.txt")
Attachment.record(result.screenshotData, named: "failure-screenshot.png")
}
#expect(result.passes)
}
종료 테스트. 종료될 것으로 예상되는 코드(exit() 호출, 치명적 오류 throw, 사전 조건 실패 트리거)를 검증하는 테스트. XCTest에는 이런 것들을 테스트할 깔끔한 방법이 없었습니다. Swift Testing의 종료 테스트는 자식 프로세스에서 코드를 실행하고 종료 동작을 검증합니다.
@Test func parserExitsOnMalformedInput() async {
await #expect(processExitsWith: .failure) {
Parser.parse(malformedInput)
}
}
두 추가 사항 모두 실용적입니다. 커스텀 첨부 파일은 실제 고통점(불안정한 CI 실패 디버깅)을 해결합니다. 종료 테스트는 단위 테스트 표면의 진정한 공백을 다룹니다.
XCTest가 여전히 옳은 선택일 때
새 코드가 여전히 XCTest여야 하는 세 가지 경우.
테스트가 UI 자동화 테스트입니다. XCUIApplication.launch, 탭과 스와이프 상호작용, 접근성 식별자 기반 쿼리. Swift Testing은 UI 자동화 표면을 다루지 않습니다. 새 UI 테스트는 XCTest입니다.
테스트가 성능 벤치마크입니다. measure(metrics: [XCTClockMetric()]), 성능 베이스라인, 메모리 측정. Swift Testing에는 동등한 것이 없습니다. 새 성능 테스트는 XCTest입니다.
테스트는 Swift Testing을 지원하지 않는 플랫폼에서 실행되어야 합니다. Swift Testing은 Swift 6 / Xcode 16+가 필요합니다. 이전 툴체인에 대해 컴파일되어야 하는 테스트(이전 Xcode 버전을 포함하는 CI 매트릭스)는 툴체인이 따라잡을 때까지 XCTest가 필요합니다.
Swift 6+ 타깃의 단위 테스트, 통합 테스트, 비동기 테스트, 파라미터화된 테스트의 경우 Swift Testing이 모던 기본값입니다. 마이그레이션 비용은 실제이지만 마이그레이션하지 않는 비용은 누적됩니다. 오늘 작성된 모든 새로운 XCTest는 기술 부채입니다.
에이전트 워크플로 연결
테스트는 AI 보조 코드 생성이 작동하는 계약입니다. 에이전트가 Swift 코드를 작성할 때, 테스트는 개발자(또는 에이전트 자체)가 변경이 올바른지 검증하는 방법입니다. Swift Testing의 속성은 에이전트가 생성한 코드를 더 검증 가능하게 만듭니다.
- 표현식 캡처.
#expect(user.score == calculator.compute(user))실패는 비교의 양쪽을 모두 보여줍니다. 실패를 읽는 에이전트는 테스트 파일을 다시 읽지 않고도 버그를 수정할 수 있습니다. - 파라미터화된 케이스. 회귀를 수정하는 에이전트는 어떤 특정 입력이 실패했는지 압니다. 수정은 정확히 겨냥할 수 있습니다.
- 트레이트 기반 필터링.
.tags(.regression)어노테이션은 에이전트가 수정 후 회귀 테스트만 실행하도록 합니다. 더 빠른 피드백 루프. - 커스텀 첨부 파일. 불안정한 테스트를 디버깅하는 에이전트는 실패한 입력 아티팩트를 첨부하고 다음 실행에서 읽을 수 있습니다.
Apple 개발용 훅 글은 모든 Claude Code 편집 후 테스트를 실행하는 Stop 훅을 다룹니다. Swift Testing의 더 빠르고, 더 병렬화되고, 더 정보가 풍부한 실패는 그 루프를 충분히 타이트하게 만들어 에이전트가 수동 개입 없이 반복할 수 있게 합니다.
이 패턴이 iOS 26+ 앱에 의미하는 것
세 가지 핵심 사항.
-
새 테스트는 Swift Testing을 기본값으로 합니다. 프레임워크는 더 표현력이 풍부하고, 기본적으로 병렬로 실행되며, 실패 컨텍스트를 자동으로 캡처하고, 파라미터화된 테스트를 일급 개념으로 지원합니다. 기존 XCTest 코드는 계속 작동합니다. 새 코드는 모던 기본값을 사용합니다.
-
UI 자동화와 성능 테스트는 XCTest에 유지합니다. Apple의 적용 범위 공백이지, 마이그레이션 선택이 아닙니다. Swift Testing이 그런 도메인으로 확장될 때까지 그 테스트들은 무기한 XCTest에 남습니다.
-
트레이트를 구성 어휘로 사용합니다.
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)는 XCTest가 클래스 계층, 명명 규칙,#if DEBUG블록을 필요로 했던 경우들을 다룹니다. 트레이트는 조합 가능한 일급 값입니다. 트레이트 어휘가 프레임워크를 모던하게 느껴지게 만드는 것입니다.
전체 Apple Ecosystem 클러스터: 타입화된 App Intents; MCP 서버; 라우팅 질문; Foundation Models; 런타임 vs 툴링 LLM 구분; 세 가지 표면; 단일 진실 공급원 패턴; 두 개의 MCP 서버; Apple 개발용 훅; Live Activities; watchOS 런타임; SwiftUI 내부; RealityKit의 공간 멘탈 모델; SwiftData 스키마 규율; Liquid Glass 패턴; 멀티 플랫폼 출시; 플랫폼 매트릭스; Vision 프레임워크; Symbol Effects; Core ML 추론; Writing Tools API; 내가 쓰기를 거부하는 것. 허브는 Apple Ecosystem Series에 있습니다. AI 에이전트가 포함된 더 폭넓은 iOS 컨텍스트는 iOS Agent Development guide를 참고하세요.
FAQ
Swift Testing이 XCTest를 완전히 대체하나요?
아직 아닙니다. XCTest는 여전히 UI 자동화 테스트(XCUIApplication)와 성능 테스트(XCTMetric)를 호스팅합니다. Swift Testing은 그런 도메인을 다루지 않습니다. 단위 테스트, 통합 테스트, 비동기 테스트, 파라미터화된 테스트의 경우 Swift Testing이 새 코드의 권장 기본값입니다. 두 프레임워크는 충돌 없이 동일한 타깃에 공존합니다.
#expect와 XCTAssertEqual의 실용적인 차이점은 무엇인가요?
#expect는 전체 Swift 표현식을 캡처하고 실패 시 비교의 양쪽을 모두 보고합니다(예: user.score == calculator.compute(user)가 user.score = 80이고 calculator.compute(user) = 100이기 때문에 실패). XCTAssertEqual은 런타임 값과 선택적 메시지 문자열만 압니다. 표현식 캡처가 Swift Testing 실패를 테스트 소스를 다시 읽지 않고도 자기 설명적으로 만드는 것입니다.
파라미터화된 테스트는 어떻게 작동하나요?
@Test(arguments: [...])는 인수당 한 번씩 테스트 함수를 실행합니다. 프레임워크는 각 인수를 자식 테스트 결과로, 부모를 롤업으로 보고합니다. 튜플은 여러 값을 전달할 수 있습니다. 파라미터화된 테스트는 기본적으로 병렬로 실행됩니다. 순차 실행을 강제하려면 .serialized를 추가하세요.
#require는 무엇을 위한 것인가요?
#require는 #expect의 빠른 실패 변형입니다. 후속 단언에서 무의미한 오류를 발생시킬 수 있는 사전 조건에 사용하세요. try #require(value)는 충족되면 언래핑된 값을 반환하거나 그렇지 않으면 throw하므로 테스트의 나머지 부분에서 언래핑된 값을 직접 사용할 수 있습니다. 언래핑을 위해 #require를 사용하려면 테스트 함수가 throws여야 합니다.
Linux에서 Swift Testing 테스트를 작성할 수 있나요?
예. Swift Testing은 오픈 소스이며2 Swift가 지원하는 모든 플랫폼(Linux 포함)에서 실행됩니다. 서버 사이드 Swift 프로젝트는 Swift Testing을 즉시 채택할 수 있습니다. 프레임워크는 Apple 플랫폼 전용이 아닙니다.
기존 XCTest 스위트를 어떻게 마이그레이션하나요?
Apple의 권장 사항은 점진적입니다. 새 테스트는 Swift Testing으로 작성하고, 오래된 것은 손댈 때 마이그레이션하세요3. 자동 변환기는 없습니다(모델 차이가 충분히 커서 자동화는 취약할 것입니다). 올바른 마이그레이션 대상은 단위 테스트와 통합 테스트입니다. UI 테스트와 성능 테스트는 무기한 XCTest에 남습니다.
참고 자료
-
Apple Developer: Swift Testing. Xcode 16+ 및 Swift 6와 함께 Swift Testing 도입에 대한 Apple의 개요. ↩
-
오픈 소스 저장소: GitHub의 swiftlang/swift-testing. 프레임워크 소스. Swift Project의 표준 라이선스와 함께 Apache License 2.0으로 제공됩니다. ↩↩
-
Apple Developer Documentation: Migrating a test from XCTest. 나란히 공존과 XCUIApplication / XCTMetric 예외를 다루는 Apple의 공식 마이그레이션 가이드. ↩↩↩↩↩↩
-
Apple Developer Documentation: Swift Testing.
@Test,@Suite,#expect,#require, 트레이트 어휘에 대한 프레임워크 레퍼런스. ↩↩ -
Apple Developer Documentation: Trait. 테스트 트레이트를 정의하는 프로토콜. 내장 트레이트에는
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)가 포함됩니다. ↩↩ -
Apple Developer: Meet Swift Testing (WWDC 2024 세션 10179) 및 Go further with Swift Testing (WWDC 2024 세션 10195). 파라미터화된 테스트와 병렬 실행을 다루는 도입 세션. ↩↩
-
WWDC 2025 추가 사항에 대한 오픈 소스 저장소 증거: 커스텀 첨부 파일에 대한
Sources/Testing/Attachments/Attachment.swift와#expect(processExitsWith:)종료 테스트 매크로에 대한Sources/Testing/ExitTests/. 둘 다 WWDC 2025에서 처음 표면화되었으며 Xcode 26+와 함께 제공됩니다. ↩↩