Swift Testing 스위트의 init() async throws에 공유 비동기 픽스처 준비 작업을 넣으세요. Swift Testing은 인스턴스 테스트 메서드를 호출하기 전에 해당 초기화를 완료하므로, 메서드는 바로 사용할 수 있는 상태를 받습니다. XCTest에서는 대신 setUp() async throws을 사용하세요.
이 가이드는 Xcode에서 로컬 단위 테스트 데이터를 준비하는 Mac 개발자를 위한 것입니다. 목표는 결정론적 계산과 합성 입력을 사용해 어설션이 시작되기 전에 비동기 초기화를 완료하는 것입니다. Apple은 XCTest에서 테스트 마이그레이션하기에서 초기화 방식을 설명합니다.
Swift Testing은 픽스처 초기화가 끝날 때까지 어떻게 기다리나요?
인스턴스 테스트 메서드의 경우 Swift Testing은 포함된 스위트 타입을 초기화한 다음 해당 인스턴스에서 메서드를 호출합니다. 스위트에는 인수 없이 호출할 수 있는 초기화 메서드가 있어야 합니다. 이 초기화 메서드는 비동기이거나, 오류를 던지거나, 둘 다일 수 있습니다. 각 인스턴스 테스트 메서드는 별도의 인스턴스에서 실행됩니다. 이는 스위트 타입으로 테스트 함수 구성하기에 문서화된 생명 주기 규칙입니다.
이로 인해 작업 순서는 명확합니다. 픽스처 준비가 반환되고, 초기화가 끝난 다음, 테스트 메서드가 시작됩니다. 초기화 메서드에서 준비 작업을 실제로 await해야 합니다. 그 안에서 작업을 시작하기만 해서는 이러한 의존성이 확립되지 않습니다.
다음 판단표를 참고해 준비 작업을 어디에 둘지 선택하세요.
| 상황 | 준비 위치 | 준비 완료를 보장하는 것 |
|---|---|---|
| 여러 인스턴스 테스트에서 같은 픽스처 레시피가 필요함 | 스위트 init() async throws |
각 메서드가 호출되기 전에 await된 초기화가 완료됨 |
| 한 테스트에서만 픽스처가 필요함 | async throws 테스트 본문 |
어설션 전에 명시적으로 try await 호출 |
기존 테스트가 XCTestCase를 상속함 |
setUp() async throws |
XCTest가 메서드 전에 비동기 설정을 완료함 |
Swift Testing 테스트가 static이거나 전역 함수임 |
테스트 본문 또는 명시적으로 호출한 헬퍼 | 테스트에서 준비 작업을 명시적으로 await함 |
이 표는 프레임워크의 문서화된 생명 주기 규칙을 픽스처 배치에 적용한 것입니다. 특히 스위트 초기화 메서드는 인스턴스마다 준비하는 지점입니다. 스위트 전체에서 한 번만 준비된다는 뜻은 아닙니다. Apple의 스위트 초기화 규칙과 XCTest 설정 순서를 참고하세요.
Xcode에서 완전한 비동기 픽스처 예제 추가하기
Swift Testing 지원이 포함된 Xcode 16 이상을 사용하세요. Swift 프로젝트 문서에 따르면 Swift Testing은 Swift 6 및 Xcode 16에 포함되어 있으므로 별도의 패키지 의존성이 필요하지 않습니다. 공식 Swift Testing README를 참고하세요.
기존 단위 테스트 타깃에서 File > New > New File From Template을 선택한 다음 Swift Testing Unit Test를 고르세요. 생성된 파일 내용을 아래의 전체 예제로 바꾸세요. 기존 XCTest 단위 테스트 번들에도 Swift Testing 테스트를 포함할 수 있으므로 별도 번들은 필요하지 않습니다. 이 단계는 Apple의 Xcode 프로젝트에 테스트 추가하기를 따릅니다.
import Testing
enum FixtureError: Error {
case invalidInput
}
private actor SquareCalculator {
let values: [Int]
init(values: [Int]) {
self.values = values
}
func squares() -> [Int] {
values.map { $0 * $0 }
}
}
struct CalculationFixture {
let squares: [Int]
let total: Int
static func make(values: [Int]) async throws -> Self {
guard !values.isEmpty,
values.count <= 100,
values.allSatisfy({ (0...100).contains($0) }) else {
throw FixtureError.invalidInput
}
let calculator = SquareCalculator(values: values)
let squares = await calculator.squares()
return Self(
squares: squares,
total: squares.reduce(0, +)
)
}
}
@Suite
struct CalculationTests {
let fixture: CalculationFixture
init() async throws {
fixture = try await CalculationFixture.make(values: [2, 4, 6])
}
@Test
func squaresAreReady() {
#expect(fixture.squares == [4, 16, 36])
}
@Test
func totalIsReady() {
#expect(fixture.total == 56)
}
}
이 예제는 실제 로컬 액터를 사용해 계산을 수행합니다. 액터 외부에서 격리된 메서드를 호출하려면 await가 필요합니다. 지연, 무작위 입력, 외부 의존성 또는 적절한 시점에 우연히 완료되어야 하는 백그라운드 작업은 없습니다. 실행 중 실제로 중단되는지와 관계없이 출력은 결정론적입니다.
픽스처 팩토리는 입력이 비어 있거나, 값이 100개를 초과하거나, 값이 0...100 범위를 벗어나면 오류를 냅니다. 이 예시의 제한은 산술 연산을 작게 유지합니다. 각 제곱값은 최대 10,000이고 총합은 최대 1,000,000입니다. 유효하지 않은 값은 조용히 제거되거나 대체되는 대신 오류를 발생시킵니다.
주어진 입력의 예상 계산 결과는 2² + 4² + 6² = 4 + 16 + 36 = 56입니다. 초기화 메서드는 완성된 픽스처를 옵셔널이 아닌 let 속성에 저장합니다. 따라서 테스트 메서드에는 준비 상태 플래그나 옵셔널 언래핑이 필요하지 않습니다.
비동기 선행 작업이 이미 초기화 메서드에서 완료되었으므로 메서드 자체는 동기식으로 유지할 수 있습니다. 비동기 작업을 테스트 본문 안에서 처리해야 하는 경우, Apple이 문서화한 방식은 해당 함수를 async으로 표시하고 작업을 직접 await하는 것입니다. 비동기 코드 테스트하기를 참고하세요.
합성 산술 결과는 Python으로 로컬에서 확인했습니다. 이 조사 환경에서는 Swift를 사용할 수 없어 Swift 예제를 검토만 하고 컴파일하거나 실행하지는 않았습니다. 위의 예상 결과는 예제의 계산값이며, Xcode 테스트를 실행한 결과가 아닙니다.
작업을 시작하는 것만으로 설정이 완료되지 않는 이유는 무엇인가요?
흔한 실수는 동기 초기화 메서드에서 Task { ... }를 시작하고 즉시 반환하는 것입니다. 그러면 초기화 메서드는 작업을 시작한 뒤 끝나지만, 테스트 메서드가 시작될 때 해당 작업은 여전히 실행 중일 수 있습니다.
Swift 언어 가이드는 Task { ... }이 구조화되지 않은 작업을 만든다고 설명합니다. 반환된 핸들을 사용하면 await handle.value를 통해 결과를 기다릴 수 있습니다. 핸들을 버리면 명시적으로 기다릴 지점이 사라집니다. 공식 Swift 책의 비구조적 동시성 섹션을 참고하세요.
픽스처 초기화에서는 비동기 초기화 메서드에서 팩토리를 직접 await하는 편이 의존성을 가장 명확하게 표현하는 경우가 많습니다. 기존 헬퍼가 내부에서 작업을 시작하고 그 작업이 끝나기 전에 반환한다면, 헬퍼를 await해도 헬퍼의 반환만 기다릴 뿐입니다. 반환 시 픽스처가 준비된 상태가 되도록 완료 계약을 바꾸거나, 호출자가 await할 수 있는 작업 결과를 노출하세요.
고정 시간 동안 잠드는 방식은 그 의존성을 표현하지 않습니다. 시간이 지나기만 기다릴 뿐이며, 준비 작업이 해당 시간 안에 끝나는지에 따라 준비 상태가 달라집니다. 마찬가지로 테스트 메서드를 async으로 표시하는 것은 비동기 호출을 가능하게 할 뿐입니다. 메서드는 여전히 의존하는 작업을 await해야 합니다.
XCTest에서는 이에 해당하는 설정을 어떻게 하나요?
XCTest는 비동기 설정 전용 훅을 제공합니다. 각 테스트 메서드 전에 실행되는 순서는 문서에 setUp() async throws, 그다음 setUpWithError(), 그다음 동기식 setUp()으로 나와 있습니다. 테스트 메서드는 이 훅들 뒤에 실행됩니다. 테스트 상태 설정 및 해제를 참고하세요.
XCTest에서 같은 픽스처를 비교하려면 첫 번째 예제와 같은 단위 테스트 타깃에 다음 전체 파일을 추가하세요. 이 파일은 XCTest를 가져오고 앞에서 정의한 모듈에 공개된 CalculationFixture를 사용합니다. 헬퍼 정의는 첫 번째 파일에 두고 중복하지 마세요.
import XCTest
final class CalculationXCTests: XCTestCase {
private var fixture: CalculationFixture?
override func setUp() async throws {
try await super.setUp()
fixture = try await CalculationFixture.make(values: [2, 4, 6])
}
func testTotalIsReady() throws {
let readyFixture = try XCTUnwrap(fixture)
XCTAssertEqual(readyFixture.squares, [4, 16, 36])
XCTAssertEqual(readyFixture.total, 56)
}
}
XCTest 메서드도 동기식으로 작성할 수 있습니다. 비동기 준비 작업이 설정 단계에서 수행되기 때문입니다. 옵셔널 속성은 XCTest의 별도 설정 단계를 수용하며, XCTUnwrap은 픽스처가 없을 때 명시적으로 실패하게 합니다. Apple은 마이그레이션 가이드에서 이 언래핑 방식을 설명합니다.
이 테스트를 Swift Testing으로 변환할 때는 준비 작업을 스위트 초기화 메서드로 옮기고 import Testing, @Test, Swift Testing의 기대값 검증을 사용하세요. 일반 Swift Testing 스위트에서 메서드 이름만 setUp으로 짓는 것은 문서화된 설정 방식이 아닙니다. Apple의 마이그레이션 경로는 설정 메서드 계열을 초기화로 대체합니다.
초기화가 여전히 일찍 끝나는 것처럼 보일 때 확인할 경계
먼저 테스트가 인스턴스 메서드인지 확인하세요. static 테스트에는 스위트 인스턴스가 필요하지 않으므로 자동 인스턴스 초기화에 의존할 수 없습니다. 전역 테스트에도 포함된 인스턴스가 없습니다. 두 형식 모두 테스트 본문에서 픽스처를 만들고 await하세요. 지원되는 테스트 형식은 XCTest에서 테스트 마이그레이션하기에 설명되어 있습니다.
둘째, 준비 작업의 전체 호출 체인을 살펴보세요. 준비된 상태를 실제로 만드는 작업을 찾아 모든 호출자가 해당 작업을 기다리도록 하세요. 예제에서는 팩토리가 반환되기 전에 액터 계산이 끝나고, 초기화가 끝나기 전에 팩토리가 반환됩니다. 어설션에 필요한 작업은 이 순서를 벗어나 실행되지 않습니다.
셋째, 설정 오류가 전파되도록 하세요. CalculationFixture.make에서 오류가 발생하면 스위트 인스턴스의 초기화가 완료되지 않으므로 해당 인스턴스의 테스트 메서드는 실행을 진행할 수 없습니다. 대체 동작이 명시적으로 테스트 대상인 경우가 아니라면 try?와 대체값을 사용해 실패한 준비 작업을 빈 픽스처로 바꾸지 마세요. 예제의 잘못된 입력 오류는 사용할 수 있는 것처럼 보이는 픽스처가 만들어지는 일을 의도적으로 막습니다.
마지막으로 초기화 순서와 테스트 스케줄링을 구분하세요. Swift Testing은 기본적으로 테스트를 병렬 실행합니다. 스위트의 .serialized 특성은 포함된 테스트를 직렬로 실행하게 할 수 있지만, 직렬화한다고 await하지 않은 작업이 기다려지는 것은 아니며 스위트 초기화가 한 번만 실행되는 것도 아닙니다. Apple은 테스트를 직렬 또는 병렬로 실행하기에서 이 특성의 적용 범위를 설명합니다.
이 로컬 계산에서는 픽스처를 불변으로 유지하고 각 인스턴스 테스트마다 독립적으로 준비하세요. 핵심은 의존성 체인입니다. 준비 작업이 완성된 픽스처를 반환하고, 초기화 메서드가 그 결과를 await하며, 테스트가 초기화된 속성을 읽습니다. 이렇게 하면 시간에 기대지 않고 구체적인 준비 완료 조건을 테스트에 제공할 수 있습니다.
출처
- 스위트 타입으로 테스트 함수 구성하기 — Apple
- XCTest에서 테스트 마이그레이션하기 — Apple
- 테스트 상태 설정 및 해제 — Apple
- Swift Testing README — Swift 프로젝트
- Xcode 프로젝트에 테스트 추가하기 — Apple
- 비동기 코드 테스트하기 — Apple
- The Swift Programming Language: Concurrency — Swift 프로젝트
- 테스트를 직렬 또는 병렬로 실행하기 — Apple