Swift Testing 완벽 가이드
Swift 6 및 Xcode 16부터 차세대 공식 테스팅 프레임워크인 Swift Testing이 기본 탑재되면서, swift test CLI 명령어와 단위 테스트 작성 방식에 표준 패러다임 전환이 일어났습니다. 기존의 Objective-C 런타임 기반 XCTest에서 벗어나 Swift 언어의 매크로 시스템과 동시성(Concurrency) 모델을 네이티브로 활용하는 테스트 환경이 구축되었습니다.
1. Swift 테스팅의 새로운 패러다임: Swift Testing vs XCTest
Apple 생태계에서 오랜 기간 사용된 XCTest는 안정적이지만, Swift 언어의 최신 기능들을 온전히 반영하지 못하는 구조적 한계가 존재했습니다. 오픈소스로 개발된 Swift Testing(import Testing)은 이러한 제약을 해결하고 크로스 플랫폼(Apple 플랫폼, Linux, Windows)에서 일관되게 동작하도록 설계되었습니다.
1) 기존 XCTest의 구조적 제약
- 클래스 상속 강제: 모든 테스트 케이스는 반드시 XCTestCase를 상속하는 참조 타입(Class)이어야 합니다.
- 이름 기반 탐색: 테스트 함수는 반드시 test 접두사로 시작해야 시스템이 인식합니다.
- 수많은 단언문 파편화: 동등성, 불리언, 널(Nil) 검사마다 XCTAssertEqual, XCTAssertTrue, XCTAssertNotNil 등 수십 개의 개별 함수를 암기해야 합니다.
- 복잡한 비동기 처리: 비동기 로직 검증 시 XCTestExpectation, wait(for:timeout:) 등의 부가 코드가 누적됩니다.
2) Swift Testing의 핵심 설계 철학
- 매크로 기반 선언: @Test, @Suite 매크로를 사용하여 구조체(struct)나 액터(actor)에서도 테스트를 선언할 수 있습니다.
- 표현식 기반 단언문 수렴: #expect와 #require 단 2개의 핵심 매크로로 모든 조건식을 판별합니다.
- 언어 내장 동시성: Swift Concurrency(async/await)와 액터 모델을 네이티브로 지원하며, 테스트가 기본적으로 병렬 실행됩니다.
- 데이터 주도 테스트 내장: 별도의 라이브러리 없이 매크로 속성(arguments:)만으로 파라미터화된 테스트를 작성할 수 있습니다.
3) 지원 환경 및 버전 호환성: Swift 6에서만 가능한가?
개발 현장에서 가장 많이 혼동하는 부분이 "Swift 6 언어 모드로 프로젝트를 완전히 마이그레이션해야만 Swift Testing을 쓸 수 있는가?"라는 점입니다. 결론부터 정리하면 다음과 같습니다.
- 툴체인(도구) 요구사항: Swift 6.0+ 및 Xcode 16.0+ 필수
Swift Testing 프레임워크(import Testing)는 Swift 6 툴체인 및 Xcode 16에 기본 번들로 포함되어 있습니다. 따라서 컴파일러와 개발 도구는 반드시 Swift 6 이상(Xcode 16 이상)이어야 합니다. Xcode 15 이하의 구버전 툴체인에서는 번들로 지원되지 않습니다. - 언어 모드(프로젝트 코드) 호환성: Swift 5 언어 모드 완벽 지원
프로젝트의 빌드 설정이 Swift Language Version: 5로 유지되어 있어도 Swift Testing을 즉시 도입할 수 있습니다. 엄격한 동시성 검사(Complete Concurrency Checking)를 당장 통과하지 못하더라도, 테스트 타깃에서 import Testing을 선언하여 점진적으로 테스트 코드부터 현대화할 수 있습니다. - 플랫폼 및 OS 최소 실행 환경
테스트 코드는 macOS 14.0+, iOS 17.0+, tvOS 17.0+, watchOS 10.0+, visionOS 1.0+ 및 Linux, Windows 환경에서 동작합니다. 단, 테스트 번들은 최종 사용자에게 배포되지 않으므로 앱 본체의 배포 타깃(iOS 15, iOS 16 등)이 낮더라도 개발 장비 및 시뮬레이터에서 정상 구동됩니다.
XCTest vs Swift Testing 핵심 사양 비교
| 구분 | XCTest (import XCTest) | Swift Testing (import Testing) |
| 필수 개발 도구 | Xcode 전기종 / 이전 Swift | Swift 6.0+ / Xcode 16.0+ |
| 언어 모드 호환성 | Swift 3 / 4 / 5 / 6 | Swift 5 모드 및 Swift 6 모드 모두 지원 |
| 최소 실행 OS | 구버전 OS 광범위 지원 | macOS 14+, iOS 17+, tvOS 17+, watchOS 10+, Linux |
| 스위트 선언 | class ...: XCTestCase (클래스 필수) | @Suite struct ... (구조체, 액터 가능) |
| 테스트 함수 선언 | func testExample() (test 접두사 필수) | @Test func example() (매크로 기반) |
| 표시 이름(Label) | 함수 식별자 이름에 의존 | @Test("사용자 인증 실패 케이스") |
| 조건 검증 | XCTAssert* 계열 다수 함수 | #expect(...), #require(...) |
| 실행 방식 | 순차 실행 기본 (설정 시 병렬) | 병렬 실행 기본 |
| 파라미터화 테스트 | 수동 루프 또는 서드파티 라이브러리 | @Test(arguments: [...]) 기본 내장 |
| 크로스 플랫폼 지원 | LinuxMain.swift 필요 등 플랫폼별 편차 | Apple OS, Linux, Windows 완전 동일 |
2. Swift Testing 핵심 문법 및 실전 작성법
Swift Testing 프레임워크는 가독성과 디버깅 편의성을 극대화하는 매크로 문법을 제공합니다.
1) @Suite와 @Test: 선언 및 표시 이름 커스텀
테스트는 클래스가 아닌 값 타입인 struct 내부에 정의할 수 있으며, 불필요한 인스턴스 공유를 원천 차단합니다.
import Testing
@Suite("주문 결제 도메인 테스트")
struct PaymentServiceTests {
@Test("유효한 결제 수단으로 결제 요청 시 승인 상태를 반환해야 한다")
func validPaymentProcessing() async throws {
let service = PaymentService()
let result = try await service.process(amount: 10_000, method: .creditCard)
#expect(result.status == .approved)
}
}
2) #expect vs #require: 조건 검증과 조기 종료
- #expect(조건): 실패하더라도 테스트 함수의 실행을 중단하지 않고 다음 라인을 계속 수행합니다. 실패 시 실패한 수식의 좌변과 우변 값을 컴파일러 매크로가 분해하여 콘솔에 시각적으로 표시합니다.
- #require(조건): 조건이 false이거나 값이 nil인 경우 즉시 에러를 throw하며 테스트를 종료합니다. 이후 코드의 전제 조건이 무너졌을 때 안전하게 테스트를 마칩니다.
@Test("주문 상세 조회 테스트")
func orderDetailLookup() throws {
let orderManager = OrderManager()
let response = orderManager.findOrder(id: "ORD-2026-001")
// response.order가 nil이면 즉시 테스트 중단 및 실패 처리
let order = try #require(response.order)
// order가 언래핑된 상태에서 후속 속성들을 검증 (실패해도 아래 검증 계속 진행)
#expect(order.items.count == 2)
#expect(order.totalAmount == 45_000)
}
3) 파라미터화된 테스트 (Parameterized Testing)
동일한 로직을 다양한 입력 데이터셋으로 반복 검증할 때 @Test(arguments:)를 사용합니다. 각 인자별 테스트는 독립된 케이스로 병렬 실행됩니다.
@Test("비밀번호 유효성 검사 규칙 검증", arguments: [
("short", false),
("validPassword123!", true),
("no_special_char12", false),
("UPPERCASEANDLOWERCASE1", false)
])
func validatePasswordRules(password: String, expectedResult: Bool) {
let validator = PasswordValidator()
#expect(validator.isValid(password) == expectedResult)
}
4) 태그(Tags) 및 트레이트(Traits)를 활용한 메타데이터 관리
테스트에 태그나 실행 조건, 타임아웃 등의 메타데이터를 부여하여 선별적으로 실행하거나 제어할 수 있습니다.
extension Tag {
@Tag static var network: Self
@Tag static var database: Self
}
@Suite(.tags(.network))
struct NetworkIntegrationTests {
@Test(
"네트워크 연결 타임아웃 검증",
.timeLimit(.minutes(1)),
.enabled(if: ServerConfig.isLiveServerReachable)
)
func apiHealthCheck() async {
let client = APIClient()
let isHealthy = await client.ping()
#expect(isHealthy)
}
}
3. Swift Package Manager swift test CLI 완전 정복
Swift Package Manager(SwiftPM) 환경에서는 터미널이나 CI 환경에서 swift test 명령어를 통해 테스트 타깃을 빌드하고 실행합니다. Swift 6 툴체인 기준 핵심 CLI 옵션과 실무 활용법은 다음과 같습니다.
1) 기본 실행 및 빌드 구조
# 기본 실행: 패키지 내 모든 테스트 타깃 빌드 및 실행
swift test
# 빌드 건너뛰고 기존 빌드 산출물로 테스트만 실행
swift test --skip-build
# 릴리즈(Release) 빌드 구성으로 최적화 성능 테스트 실행
swift test -c release
2) 특정 테스트 선별 실행 (--filter / --skip)
정규표현식을 지정하여 특정 모듈, 스위트, 또는 개별 테스트 함수만 골라 실행하거나 제외할 수 있습니다.
# PaymentServiceTests 스위트만 실행
swift test --filter PaymentServiceTests
# 특정 테스트 함수만 지정하여 실행
swift test --filter PaymentServiceTests.validPaymentProcessing
# 무거운 성능 테스트(PerformanceTests) 제외하고 실행
swift test --skip PerformanceTests
3) 병렬 테스트 실행 제어 (--parallel / --num-workers)
테스트 실행 속도를 단축하기 위해 병렬 처리를 명시적으로 활성화하고 워커 프로세스 수를 조정합니다.
# 병렬 실행 활성화
swift test --parallel
# 동시 실행 워커 수(프로세스 수)를 4개로 제한
swift test --parallel --num-workers 4
4) Flaky 테스트 감지 및 반복 실행 (--maximum-repetitions / --repeat-until)
간헐적으로 실패하는(Flaky) 불안정한 테스트를 검증할 때 사용하는 Swift Testing 전용 CLI 기능입니다.
# 테스트가 실패할 때까지 최대 50회 반복 실행
swift test --maximum-repetitions 50 --repeat-until fail
# 실패한 테스트가 통과할 때까지 최대 5회 재시도
swift test --maximum-repetitions 5 --repeat-until pass
5) 코드 커버리지 측정 (--enable-code-coverage)
테스트 코드의 소스 커버리지를 계산하고 JSON 형식의 경로를 추출할 수 있습니다.
# 코드 커버리지 수집 활성화
swift test --enable-code-coverage
# 생성된 코드 커버리지 프로파일 JSON 파일 경로 출력
swift test --show-code-coverage-path
6) 메모리 및 동시성 버그 검출 (--sanitize)
LLVM 런타임 Sanitizer를 결합하여 Data Race, 버퍼 오버플로우 등의 저수준 결함을 검출합니다.
# 스레드 동시성 충돌(Data Race) 감지
swift test --sanitize=thread
# 잘못된 메모리 주소 참조(Use-after-free 등) 감지
swift test --sanitize=address
7) 프레임워크 선택 및 테스트 목록 확인
# 등록된 모든 테스트 메서드 식별자 목록 출력
swift test --list-tests
# Swift Testing만 활성화하고 레거시 XCTest 비활성화
swift test --enable-swift-testing --disable-xctest
4. CI/CD 환경에서의 swift test 자동화 파이프라인 구축
GitHub Actions 기반 CI 파이프라인에서 Linux와 macOS 매트릭스를 구성하여 크로스 플랫폼 테스트를 자동화하는 예제입니다.
name: Swift Test CI
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
jobs:
test:
name: Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15]
steps:
- name: Checkout Code
uses: actions/checkout@v4
# Ubuntu 환경일 경우 공식 Swift 툴체인 설정
- name: Set up Swift
if: runner.os == 'Linux'
uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0"
# 테스트 실행 및 결과 리포트 추출
- name: Run Swift Tests
run: |
swift test \
--parallel \
--enable-code-coverage \
--xunit-output build/reports/results.xml
# xUnit 테스트 결과 아티팩트 업로드
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}
path: build/reports/results.xml
5. XCTest에서 Swift Testing으로의 점진적 마이그레이션 전략
대규모 프로젝트에서는 기존 XCTest 코드를 한 번에 교체하기 어렵습니다. Apple 공식 문서에 따르면 두 프레임워크는 동일한 테스트 타깃 안에서 공존(Side-by-side)할 수 있으므로 점진적 전환이 권장됩니다.
1) 마이그레이션 핵심 변환 치트시트
| XCTest (기존) | Swift Testing (최신) | 변환 목적 및 특징 |
| import XCTest | import Testing | 프레임워크 모듈 교체 |
| class FooTests: XCTestCase | @Suite struct FooTests | 참조 타입에서 값 타입으로 변경 |
| func testAddition() | @Test func addition() | 접두사 제거 및 매크로 선언 |
| override func setUp() | init() async throws | 인스턴스 초기화 구문으로 대체 |
| override func tearDown() | deinit | 해제 구문으로 리소스 정리 대체 |
| XCTAssertEqual(a, b) | #expect(a == b) | 단순 비교 연산자로 통일 |
| XCTAssertNotNil(value) | let val = try #require(value) | 옵셔널 바인딩과 즉시 실패 결합 |
| XCTAssertTrue(cond) | #expect(cond) | 불리언 표현식 직접 판별 |
| XCTAssertNil(value) | #expect(value == nil) | 표준 nil 비교 |
| XCTAssertThrowsError(try fn()) | #expect(throws: ExpectedError.self) { try fn() } | 에러 타입 직접 검증 |
2) 마이그레이션 시 주의사항
- setUpWithError() / tearDownWithError(): Swift Testing 구조체에서는 init() 및 deinit을 사용하며, 비동기 초기화가 필요할 경우 init() async throws를 선언할 수 있습니다.
- XCTestExpectation 제거: 콜백 기반의 대기 로직은 withCheckedContinuation을 활용해 Swift Concurrency(async) 함수로 래핑한 뒤 #expect로 직접 대기합니다.
3) UI 테스트 및 성능 테스트 지원 여부: Swift Testing에서 불가능한 이유와 대안
- UI 자동화 테스트(XCUIApplication) 지원 불가:
Swift Testing 프레임워크는 순수 비즈니스 로직 단위 테스트(Unit Test) 및 비동기 통합 테스트(Integration Test) 전용으로 설계되었습니다. 앱 프로세스를 직접 실행하여 화면 탭, 스크롤, 제스처, 접근성(Accessibility) 요소를 조작하는 XCUIApplication 기반의 UI 테스트는 Swift Testing에서 지원하지 않으며, 계속해서 XCTest를 사용해야 합니다. - 기술적 배경:
XCUIApplication은 Apple OS 전용의 프로세스 간 통신(IPC) 및 시스템 Accessibility 계층과 강하게 결합되어 있습니다. 반면 Swift Testing은 Apple 플랫폼뿐만 아니라 Linux, Windows 등 크로스 플랫폼 환경에서 순수 Swift 표준 라이브러리 및 동시성 런타임 위에서 동작하도록 설계되었기 때문에 플랫폼 종속적인 UI 자동화 제어 엔진을 포함하지 않습니다. - 성능 벤치마크 테스트(measure, XCTMetric):
실행 시간, 메모리 할당량, CPU 사이클 등을 측정하는 성능 벤치마크 API 역시 현재는 XCTest에서만 제공됩니다. - 실무 권장 테스트 타깃 구성 아키텍처:
- 단위/통합 테스트 타깃(Unit Test Target): Swift Testing(@Test, #expect)을 주력으로 전환하여 빠른 병렬 실행과 간결한 매크로 문법의 이점을 취합니다.
- UI 테스트 타깃(UI Test Target): Xcode의 UI Test Target을 그대로 유지하고, XCTestCase와 XCUIApplication을 사용하여 E2E(End-to-End) UI 시나리오를 검증합니다.
- 순수 View 로직 단위 테스트: 화면 렌더링 시뮬레이션이 아닌 SwiftUI 뷰의 상태 전이, ViewModel 검증, 또는 오픈소스 뷰 파싱 라이브러리(ViewInspector 등)를 통한 뷰 계층 구조 검증은 단위 테스트 타깃 내에서 Swift Testing으로 작성할 수 있습니다.
6. 결론
Swift Testing은 매크로 기반의 직관적인 문법과 언어 내장 동시성 모델을 통해 단위 테스트 작성 비용을 대폭 절감하며, Swift Package Manager의 swift test CLI 도구와 결합하여 로컬 개발 환경부터 크로스 플랫폼 CI/CD 파이프라인까지 고성능 자동화 테스트 체계를 구축할 수 있습니다.
'개발 > Swift' 카테고리의 다른 글
| Swift 6 렌더링 최적화: ObservableObject에서 @Observable 마이그레이션 가이드 (0) | 2026.08.20 |
|---|---|
| Swift 6 Strict Concurrency 대응: SwiftData ModelActor 비동기 파싱 및 저장 가이드 (0) | 2026.08.05 |
| Xcode 27 / Swift 6.3 SPM 동적 라이브러리(Dynamic Framework) 링킹 충돌 원인과 해결 방법 (0) | 2026.08.04 |
| Swift 6 데이터 레이스 컴파일 에러 해결: MainActor와 Sendable 실무 패턴 (0) | 2026.07.31 |
| TCA Study - State · Action · Reducer · Store — TCA의 핵심 구조 완전 정복 (0) | 2025.12.16 |
| TCA Study - Swift 개발자가 TCA를 알아야 하는 이유 (0) | 2025.12.16 |
| Swift에서 정렬: sort vs sorted (0) | 2025.11.11 |
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |


