Mac에서 XCTest 단위 테스트를 마이그레이션할 때는 소유권 순서를 보존하세요. 작업을 마치고, 의도한 강한 참조를 해제한 다음, 약한 참조를 확인합니다. 동기식 정리의 경우 테스트 내부의 defer를 사용하면 이 순서를 명확히 표현할 수 있습니다.
이 안내에서는 Xcode에서 소유권에 민감한 단위 테스트를 변환하는 방법을 살펴봅니다. 예제는 합성 그리드의 셀 수를 계산한 다음 계산기가 해제되는지 확인합니다. 로컬 Swift 객체와 정수 연산만 사용합니다.
마이그레이션에서 어떤 생명 주기 동작을 보존해야 하나요?
XCTest는 프레임워크가 관리하는 순서를 제공합니다. 테스트 메서드가 끝나면 등록된 teardown 블록을 등록 순서의 역순으로 실행한 다음 인스턴스 teardown 메서드를 실행합니다. 테스트 assertion이 실패해도 등록된 정리 코드는 실행되지만, 프로세스가 종료되면 실행되지 않을 수 있습니다. 이 세부 사항은 한 블록에서 객체를 해제하고 다른 블록에서 해당 객체의 약한 참조를 확인할 때 중요합니다. 테스트 상태 설정 및 해제
예를 들어 nil을 확인하는 teardown 블록은 마지막으로 의도된 소유자를 제거하는 블록 다음에 실행되어야 합니다. XCTest는 등록 순서의 역순으로 실행하므로 확인 블록을 먼저 등록하고 해제 블록을 나중에 등록합니다. 등록 순서를 바꾸면 테스트가 관찰하는 결과도 달라집니다.
Swift Testing의 마이그레이션 가이드는 클래스 또는 액터 스위트에서 설정을 이니셜라이저에, teardown을 디이니셜라이저에 대응시킵니다. 이 대응은 스위트의 생명 주기와 관련된 정리에 유용합니다. 소유권 assertion에는 추가로 결정할 사항이 있습니다. assertion 실행 전에 정확히 어떤 참조가 사라져야 할까요? XCTest에서 테스트 마이그레이션하기
테스트 구문을 바꾸기 전에 다음 결정표를 사용하세요.
| 기존 책임 | 마이그레이션 선택 | 이유 |
|---|---|---|
| 각 인스턴스 테스트에 사용할 새 fixture 상태 구성 | 저장 프로퍼티 초기화 또는 init() |
테스트 실행 전에 fixture를 설정합니다 |
| 객체를 해제하고 즉시 약한 참조 확인 | 명시적 해제 후 #expect 실행. 보통 로컬 defer 내부에서 처리 |
관찰 시점을 명확히 보여 줍니다 |
| 참조 타입 스위트의 생명 주기에 따른 동기식 정리 | deinit를 사용하는 클래스 또는 액터 스위트 |
문서화된 생명 주기 대응을 따릅니다 |
| 정리를 기다리거나 정리 오류 보고 | 비동기 함수에서 정리를 기다리거나 적절한 사용자 지정 스코프 사용 | 일시 중단과 오류 처리를 지원하는 실행 경로가 필요합니다 |
이 표는 문서화된 생명 주기와 언어 규칙에 기반한 마이그레이션 의사결정 도구입니다. 모든 XCTest 훅에 서로 바꿔 쓸 수 있는 Swift Testing 메서드가 있다는 뜻은 아닙니다.
약한 참조 확인을 deinit 안으로 옮기면 테스트가 달라질 수 있는 이유는 무엇인가요?
Swift는 클래스 인스턴스를 할당 해제하기 직전에 디이니셜라이저를 호출합니다. 디이니셜라이저는 그 시점에도 인스턴스의 저장 프로퍼티에 접근할 수 있습니다. 따라서 스위트의 deinit에 진입했다고 해서 해당 프로퍼티가 보유한 객체가 이미 해제되었다는 뜻은 아닙니다. 초기화 해제
강한 layout 프로퍼티가 있는 스위트를 생각해 보세요. 해당 레이아웃에 대한 약한 참조는 스위트의 디이니셜라이저가 실행되는 동안에도 nil가 아닌 상태로 남을 수 있습니다. 레이아웃이 해제되었는지 확인하기 전에 테스트에서 강한 프로퍼티 참조를 제거해야 합니다.
또 다른 실패 상황은 순환 참조가 스위트를 유지해 디이니셜라이저 실행을 막는 경우입니다. 이때 디이니셜라이저 안에만 둔 assertion은 참조가 유지되고 있음을 보고할 기회를 얻지 못합니다. 이는 ARC의 소유권 규칙에 따른 결과입니다. 강한 참조는 인스턴스를 계속 유지하고, 약한 참조는 할당 해제를 막지 않으며 참조 대상 인스턴스가 할당 해제되면 nil가 됩니다. 자동 참조 카운팅
소유권 테스트에서는 해제를 확인할 객체 바깥에 관찰자를 두세요. 테스트에 구체적인 해제 작업을 제공하고 테스트의 정상적인 제어 흐름 안에서 assertion을 실행하세요. 이렇게 하면 스위트 자체의 할당 해제에 의존하지 않고 테스트 계약을 확인할 수 있습니다.
Xcode에서 XCTest 기준 테스트 설정하기
Xcode 16 이상에서 기존 단위 테스트 타깃을 사용하세요. Apple 문서에 따르면 XCTest와 Swift Testing은 같은 타깃 및 번들에서 함께 사용할 수 있으므로 마이그레이션을 위해 별도의 단위 테스트 번들이 필요하지 않습니다. 테스트 파일을 추가하려면 File > New > New File From Template을 선택하고 적절한 항목인 XCTest Unit Test 또는 Swift Testing Unit Test를 고르세요. Xcode 프로젝트에 테스트 추가하기
다음은 완전한 XCTest 테스트 파일입니다. 계산기는 3 * 4를 반환하므로 예상 결과는 12입니다. 두 teardown 등록은 필요한 실행 순서를 의도적으로 보여 줍니다.
import XCTest
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
final class GridCalculatorXCTests: XCTestCase {
private var calculator: GridCalculator?
override func setUp() {
super.setUp()
calculator = GridCalculator()
}
func testCellCountAndRelease() {
weak var observed = calculator
// Registered first, so this executes second.
addTeardownBlock { [weak observed] in
XCTAssertNil(observed)
}
// Registered second, so this executes first.
addTeardownBlock { [self] in
calculator = nil
}
XCTAssertEqual(
calculator?.cellCount(columns: 3, rows: 4),
12
)
}
}
해제 블록은 fixture 프로퍼티를 업데이트할 수 있도록 테스트 케이스를 강하게 유지합니다. 관찰 블록은 계산기를 약하게 캡처하므로 확인 과정에서 추가적인 강한 소유자가 생기지 않습니다. Apple 문서에서는 관련 설정과 함께 등록하는 테스트별 정리 작업에 teardown 블록을 사용하도록 안내합니다. 테스트 상태 설정 및 해제
기준 테스트의 계약은 좁게 정의되어 있습니다. 테스트 본문이 끝나고 fixture 프로퍼티를 비운 뒤에는 계산기가 더 이상 존재하지 않아야 합니다. 메모리 소비량을 측정하거나 애플리케이션에서 만든 모든 객체가 해제되었음을 입증하지는 않습니다.
소유권 순서를 로컬 스코프로 마이그레이션하기
이 동기식 사례에서는 구조체 스위트를 사용하고 해제와 검증을 하나의 defer 블록에 넣으세요. Swift는 제어가 해당 스코프를 벗어날 때, 조기 반환이나 오류 발생의 경우를 포함해 지연된 코드를 실행합니다. 여러 지연 블록은 역순으로 실행됩니다. 블록 하나를 사용하면 이 예제의 해제 후 확인 순서를 더 쉽게 읽을 수 있습니다. 제어 흐름
다음은 독립적으로 실행할 수 있는 완전한 Swift Testing 파일입니다. 기준 파일을 이 파일로 교체하거나, private 도우미 타입을 별도로 유지할 수 있도록 예제를 서로 다른 파일에 둘 수 있습니다. 두 번째 테스트는 진단용 대조 사례입니다. 추가적인 강한 소유자가 있으면 해당 소유자를 비울 때까지 약한 참조에 값이 남아 있음을 보여 줍니다.
import Testing
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
private final class GridFixture {
var calculator: GridCalculator? = GridCalculator()
var additionalOwner: GridCalculator?
}
struct GridCalculatorTests {
@Test
func cellCountAndRelease() throws {
let fixture = GridFixture()
weak var observed = fixture.calculator
defer {
fixture.calculator = nil
#expect(observed == nil)
}
let result = try #require(
fixture.calculator?.cellCount(columns: 3, rows: 4)
)
#expect(result == 12)
}
@Test
func weakProbeDetectsAnAdditionalOwner() {
let fixture = GridFixture()
weak var observed = fixture.calculator
fixture.additionalOwner = fixture.calculator
fixture.calculator = nil
#expect(observed != nil)
fixture.additionalOwner = nil
#expect(observed == nil)
}
}
첫 번째 테스트에서 fixture는 계산기를 소유하고 observed는 소유권 없이 계산기를 관찰합니다. 계산 결과는 옵셔널 정수이며 #require가 옵셔널을 해제합니다. 옵셔널에서 해제한 계산기는 다른 로컬 변수에 저장하지 않습니다. 스코프를 벗어나면 지연 블록이 fixture의 참조를 비우고 관찰자를 확인합니다.
#expect가 실패하면 이슈를 기록하고 실행을 계속합니다. #require가 실패하면 오류를 던져 해당 실행 경로를 중단합니다. defer은 require보다 먼저 등록되므로 require가 오류를 던져도 정리와 약한 참조 확인은 실행됩니다. 기대 및 확인, 오류 처리
진단용 대조 사례는 서로 다른 두 가지 예상 상태를 보여 줍니다. 소유자 둘 중 하나를 비워도 관찰자는 nil가 아닌 상태를 유지하고, 남은 소유자까지 비우면 nil가 됩니다. 이는 소유권 확인을 설명하기 위한 예시이며 애플리케이션 측정 결과가 아닙니다. Swift 실행 파일을 사용할 수 없어 연구 환경에서 Swift 예제를 컴파일하지는 못했지만 내용을 검토했습니다.
도우미의 경계, 동시성, 비동기 정리 확인하기
로컬 defer는 이를 둘러싼 스코프에 속합니다. 지연 블록을 팩토리 도우미로 옮기면 도우미가 반환될 때 실행되므로 테스트에서 반환된 객체를 사용하기 전에 실행될 수 있습니다. 블록을 테스트 안에 두거나 전체 작업을 도우미의 스코프 안에서 실행하는 도우미를 설계하세요. 바깥 스코프는 소유권 계약의 일부입니다. 제어 흐름
fixture가 어디에 있는지도 확인하세요. Swift Testing은 인스턴스 테스트 메서드마다 별도의 스위트 인스턴스를 만들며, 기본적으로 스위트 내 테스트를 병렬로 실행합니다. 인스턴스별 또는 테스트별 상태는 이 모델에 적합하지만, 전역 fixture나 static 컬렉션은 테스트 간에 공유되는 소유권을 만듭니다. 스위트 타입으로 테스트 함수 구성하기
메인 액터가 필요한 Mac 애플리케이션 코드에서는 Swift Testing 테스트에 @MainActor를 명시적으로 적용하세요. 마이그레이션 가이드에 따르면 Swift Testing은 동기식 테스트 메서드에 대한 XCTest의 기본 메인 액터 동작을 상속하지 않습니다. 위의 합성 계산기에는 그런 요구 사항이 없습니다. XCTest에서 테스트 마이그레이션하기
동기식 지연 블록은 대기해야 하는 teardown을 대체하지 않습니다. Swift의 지연 작업은 블록 밖으로 오류를 던질 수 없습니다. 정리가 실패할 수 있다면 오류를 버리지 말고 명시적으로 처리하고 보고하세요. 대기해야 하는 정리는 비동기 실행 경로 안에 두고, 본문이 성공하거나 오류를 던지는 양쪽 경로에서 실행되도록 하세요. 오류 처리
재사용 가능한 프레임워크 관리 설정과 teardown을 위해 현재 Swift Testing은 테스트 또는 스위트 주변에 사용자 지정 실행을 제공하는 TestScoping을 제공합니다. Apple 문서에는 Swift 6.1 및 Xcode 16.3에서 사용할 수 있다고 나와 있습니다. 여러 테스트에 공통 스코프가 필요할 때 지원되는 방식이지만 addTeardownBlock의 이름만 바꾸는 것으로는 충분하지 않으며 사용자 지정 trait와 스코프 제공자가 필요합니다. TestScoping
마이그레이션 후 약한 참조 확인 결과가 달라지는 문제 진단하기
마이그레이션한 소유권 테스트가 실패하면 다음 항목을 순서대로 살펴보세요.
- 관찰 시점을 확인합니다. 약한 참조 assertion 전에 의도한 해제가 이루어져야 합니다.
- fixture 프로퍼티를 살펴봅니다. 다른 프로퍼티를 비웠더라도 남아 있는 프로퍼티가 계산기를 계속 소유할 수 있습니다.
- 로컬 별칭과 클로저 캡처를 살펴봅니다. 옵셔널에서 해제한 객체나 강하게 캡처한 참조가 확인 시점까지 소유권을 유지할 수 있습니다.
- 도우미 스코프를 살펴봅니다. 도우미의 지연 정리가 테스트가 의도한 계약에 비해 너무 일찍 실행될 수 있습니다.
- 완료되지 않은 작업을 살펴봅니다. 실제 객체가 비동기 작업을 시작한다면 동기식 해제 조건을 적용하기 전에 작업 완료를 정의하고 기다리세요.
nil가 아닌 약한 참조는 관찰 시점에 해당 객체가 여전히 존재함을 나타냅니다. assertion을 바꾸거나 지연을 추가하기 전에 남은 소유자를 조사하세요. 이 합성 예제에는 기다릴 비동기 작업이 없으며 진단용 대조 사례에서 추가 소유자가 결과에 어떤 영향을 주는지 정확히 확인할 수 있습니다.
마이그레이션이 완료되려면 계산과 소유권 assertion이 기준 테스트와 같은 의도된 결과를 표현해야 합니다. 12를 계산하고, fixture의 의도된 소유권을 제거한 다음, 별도로 유지한 약한 참조를 통해 해제를 확인합니다. 테스트가 발전하더라도 이 순서를 명확히 유지하세요.
출처
테스트 상태 설정 및 해제 — Apple 개발자 문서.
XCTest에서 테스트 마이그레이션하기 — Swift 프로젝트.
초기화 해제 — Swift 프로젝트, The Swift Programming Language.
자동 참조 카운팅 — Swift 프로젝트, The Swift Programming Language.
Xcode 프로젝트에 테스트 추가하기 — Apple 개발자 문서.
제어 흐름 — Swift 프로젝트, The Swift Programming Language.
기대 및 확인 — Swift 프로젝트.
오류 처리 — Swift 프로젝트, The Swift Programming Language.
스위트 타입으로 테스트 함수 구성하기 — Swift 프로젝트.
TestScoping — Apple 개발자 문서.