Mac에서 무작위 Swift 테스트 입력을 재현하려면 각 테스트 사례마다 새 생성기에 명시적 시드를 전달하세요. 생성기, 입력 매핑, 난수 추출 순서를 고정하고 모든 실패와 함께 시드를 기록하세요.
이 안내에서는 합성 산술 계산을 검사하는 작은 로컬 Swift 패키지를 만듭니다. 완전한 Swift Testing 예제와 XCTest 대안을 제공합니다. 모든 테스트 데이터는 로컬 계산에서 생성되며, 패키지에는 외부 종속성이 없습니다.
시드는 실제로 무엇을 반복 가능하게 만드나요?
변동의 원인은 두 가지로 나뉩니다. 테스트가 생성하는 값과 테스트 실행기가 테스트를 예약하는 순서입니다. 애플리케이션에서 정의한 시드는 생성된 모든 값이 해당 생성기에서 나오는 경우에만 첫 번째 원인을 제어합니다. Apple 문서에 따르면 일반적인 난수 연산은 기본적으로 자동 시드가 설정된 SystemRandomNumberGenerator를 사용합니다. 문서에 명시된 이니셜라이저는 시드를 받지 않습니다. SystemRandomNumberGenerator
Xcode는 테스트 간 종속성을 드러내기 위해 무작위 실행 순서도 제공합니다. 테스트 계획 문서에는 알파벳순 또는 무작위 실행 순서가 설명되어 있지만, 해당 설정에 사용자 지정 재현 시드를 제공하는 방법은 문서화되어 있지 않습니다. 따라서 여기서는 독립적인 테스트 안에서 명시적으로 시드가 지정된 입력 생성기를 사용하는 지원 경로를 활용합니다. 이 방법은 실행기의 예약 순서를 재현한다고 보장하지 않습니다. 테스트 계획으로 테스트를 구성해 코드 평가 개선하기
불안정한 테스트를 변경하기 전에 다음 기준을 참고하세요.
| 실행마다 무엇이 달라지나요? | 제어할 항목 |
|---|---|
| 생성된 숫자 입력 | 시드, 생성기 구현, 입력 매핑, 난수 추출 순서 |
| 다른 테스트가 실행된 뒤의 결과 | 변경 가능한 상태와 테스트별 초기화 |
| 동시 실행 중에만 나타나는 결과 | 공유 변경 가능 리소스와 겹치는 작업 |
| 툴체인 업데이트 후의 입력 | 입력 매핑 구현과 기록된 구체적 입력 |
Swift는 RandomNumberGenerator를 통해 사용자 지정 생성기를 명시적으로 지원합니다. 이 프로토콜에서 필수로 구현해야 하는 연산은 UInt64를 반환하는 변경 가능한 next()입니다. Swift 프로젝트의 소스 문서에서는 테스트용으로 반복 가능한 시퀀스를 만드는 것을 시드 지정 가능 생성기를 제공하는 이유로 들고 있습니다. Random.swift
작은 로컬 테스트 패키지 만들기
Xcode와 Swift 6 이상 툴체인을 사용할 수 있는 Mac을 준비하세요. 터미널에서 선택된 명령줄 툴체인을 확인한 다음 새 디렉터리를 만들고 라이브러리 패키지를 초기화합니다.
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
Swift 프로젝트의 통합 테스트는 swift package init --type library를 사용해 라이브러리 패키지를 생성하는 절차를 검증합니다. Swift 6에는 Swift Testing이 기본 포함되어 있으므로, 이 예제는 import Testing을 위해 패키지 종속성을 추가할 필요가 없습니다. Swift Package Manager는 Swift Testing 테스트와 XCTest 테스트가 있으면 둘 다 실행합니다. Swift 패키지 초기화 통합 테스트, Swift 6 발표
생성된 패키지 매니페스트와 라이브러리 소스는 그대로 둡니다. 생성된 샘플 테스트 파일을 아래 테스트 파일로 바꾸고 모두 Tests/SeedReplayTests에 둡니다. 이 예제를 독립적으로 유지하기 위해 산술 함수는 테스트 지원 파일에 넣습니다. 기존 프로젝트에서는 실제로 테스트할 함수를 사용하세요.
시드와 입력 생성 규약 정의하기
Tests/SeedReplayTests/ReplaySupport.swift를 만들고 다음 내용을 모두 넣습니다.
import Foundation
// Local synthetic test-data generator.
// Mutable value state: give each test case its own instance.
// Its output sequence is part of this example's replay contract.
struct ReplayGenerator: RandomNumberGenerator {
private var state: UInt64
init(seed: UInt64) {
state = seed
}
mutating func next() -> UInt64 {
state &+= 0x9E3779B97F4A7C15
var value = state
value = (value ^ (value >> 30)) &* 0xBF58476D1CE4E5B9
value = (value ^ (value >> 27)) &* 0x94D049BB133111EB
return value ^ (value >> 31)
}
}
enum ReplayConfigurationError: Error {
case invalidSeed(String)
}
func replaySeeds() throws -> [UInt64] {
guard let text = ProcessInfo.processInfo.environment["TEST_SEED"] else {
return [0, 42, 2026]
}
guard !text.isEmpty,
text.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }),
let seed = UInt64(text) else {
throw ReplayConfigurationError.invalidSeed(text)
}
return [seed]
}
func generatedInputs(seed: UInt64) -> [Int] {
var generator = ReplayGenerator(seed: seed)
return (0..<256).map { _ in
// Exactly one draw per input; retain the upper ten bits.
Int(generator.next() >> 54)
}
}
func triangularNumber(_ n: Int) -> Int {
precondition((0...1023).contains(n))
return n * (n + 1) / 2
}
func referenceTotal(_ n: Int) -> Int {
precondition((0...1023).contains(n))
return (0...n).reduce(0, +)
}
설정은 0을 포함한 십진수 UInt64를 받습니다. TEST_SEED이 설정되지 않으면 명시적 시드 세 개를 선택합니다. 값이 비어 있거나 음수이거나 숫자가 아니거나 범위를 초과하면 오류가 발생합니다. 따라서 시드를 잘못 입력해도 다른 실행이 조용히 선택되지 않습니다.
생성기는 명시적 래핑 연산을 사용합니다. 생성되는 입력마다 출력 하나의 상위 10비트를 취하므로 값은 0부터 1023까지입니다. 이 매핑은 소스 코드에서 의도적으로 고정합니다. 산술 값은 작은 범위에 머뭅니다. 최댓값인 예상 결과는 523776입니다. 이는 범위가 제한된 합성 입력 레시피이며, 통계적 테스트 커버리지를 주장하는 것은 아닙니다.
여기서는 왜 Int.random(in:using:)을 피할까요? 이 API는 지정된 생성기를 지원하므로 고정된 툴체인에서 재현할 때 유용합니다. 하지만 Apple은 동일한 내부 시퀀스를 생성하는 생성기를 사용하더라도 값 생성 알고리즘이 Swift 버전 간에 바뀔 수 있다고 명시적으로 경고합니다. 이 예제의 간단한 매핑을 직접 구현하면 해당 종속성을 제거할 수 있습니다. Int.random(in:using:)
Swift Testing 버전 실행 및 재현하기
Tests/SeedReplayTests/SeedReplayTests.swift를 만듭니다.
import Testing
@Test(arguments: try replaySeeds())
func triangularTotals(seed: UInt64) {
print("triangularTotals seed=\(seed) recipe=upper10-v1 count=256")
for (index, n) in generatedInputs(seed: seed).enumerated() {
let actual = triangularNumber(n)
let expected = referenceTotal(n)
#expect(
actual == expected,
"seed=\(seed) case=\(index) n=\(n) actual=\(actual) expected=\(expected)"
)
if actual != expected {
return
}
}
}
@Test
func replayRecipeIsStable() {
var generator = ReplayGenerator(seed: 0)
let firstWord = generator.next()
#expect(firstWord == 0xE220A8397B1DCDAF)
let firstInputs = Array(generatedInputs(seed: 0).prefix(3))
#expect(firstInputs == [904, 441, 27])
}
매개변수화된 테스트는 각 시드를 별도의 테스트 사례로 만듭니다. Apple 문서에 따르면 arguments:는 컬렉션 요소를 테스트 호출에 전달하며 오류를 던질 수 있는 표현식도 허용합니다. 따라서 테스트 모델에서 시드 선택을 명확히 확인할 수 있습니다. 명시적 루프 인덱스를 사용하면 해당 시드에서 개별 생성 입력을 식별할 수 있습니다. 매개변수화된 테스트 구현하기
기본 컬렉션을 실행한 다음, 시드 하나를 두 번 재현합니다.
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
소스가 바뀌지 않았다면 이 두 번의 시드 지정 실행에서 동일한 숫자 입력이 생성되어야 합니다. 시드 42에서 처음 세 입력은 759, 163, 285입니다. 이 테스트는 해당 시드로 입력 256개를 검사합니다. 별도의 안정성 테스트는 선택한 산술 테스트 시드와 무관하게 생성기와 매핑을 확인하도록 시드를 0으로 고정합니다.
제공된 구현에서는 모든 단언이 통과해야 합니다. 조사 환경에서는 Swift를 사용할 수 없어 Swift 파일을 컴파일하지 않고 검토만 했습니다. 별도의 Python 계산으로 기본 시드 세 개의 산술 비교 768개와 표시된 초기 입력값을 확인했습니다. 이 확인은 Swift 패키지 컴파일을 대신하지 않습니다.
의도적인 계산 오류를 재현해 보기
진단 절차를 확인하려면 triangularNumber(_:)의 반환 표현식을 다음 레이블이 붙은 코드 조각으로 임시 교체합니다.
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
TEST_SEED=42 swift test을 다시 실행합니다. 첫 생성 입력은 759입니다. 변경된 표현식의 결과는 288040이고 기준 합계는 288420입니다. 단언 메시지에는 시드 42, 사례 인덱스 0, 입력 759이 표시됩니다. 조기 반환을 사용하므로 첫 불일치를 기록한 뒤 해당 호출이 종료되어 진단에 집중할 수 있습니다.
return n * (n + 1) / 2을 복원하고 같은 명령을 다시 실행합니다. 수정 사항을 평가하는 동안 시드는 일정하게 유지하세요. 생성된 입력에서 유용한 결함을 찾았다면 해당 구체적 입력을 이름 있는 회귀 테스트에도 보존하세요. 나중에 생성 레시피가 바뀌어도 그 입력은 이해하기 쉽습니다.
XCTest에도 같은 수명 주기 적용하기
XCTest 기반 타깃에서는 ReplaySupport.swift를 그대로 두고, Swift Testing 파일 대신 다음 전체 테스트 파일을 사용합니다. 파일 이름은 Tests/SeedReplayTests/SeedReplayXCTests.swift로 지정합니다.
import XCTest
final class SeedReplayXCTests: XCTestCase {
func testTriangularTotals() throws {
for seed in try replaySeeds() {
print("testTriangularTotals seed=\(seed) recipe=upper10-v1 count=256")
for (index, n) in generatedInputs(seed: seed).enumerated() {
let actual = triangularNumber(n)
let expected = referenceTotal(n)
XCTAssertEqual(
actual,
expected,
"seed=\(seed) case=\(index) n=\(n)"
)
if actual != expected {
return
}
}
}
}
func testReplayRecipeIsStable() {
var generator = ReplayGenerator(seed: 0)
XCTAssertEqual(generator.next(), 0xE220A8397B1DCDAF)
XCTAssertEqual(
Array(generatedInputs(seed: 0).prefix(3)),
[904, 441, 27]
)
}
}
동일한 터미널 명령을 사용하면 됩니다. generatedInputs(seed:)를 호출할 때마다 새 생성기를 만들기 때문에 다른 테스트가 다른 테스트의 난수 스트림을 소비하지 않습니다. 테스트 후 정리해야 할 생성기 상태도 없습니다.
기존 XCTest 픽스처에서 생성기를 속성으로 보관한다면 각 테스트 메서드 전에 초기화하세요. Apple 문서에서는 테스트별 초기화에는 인스턴스 설정 메서드를, 클래스에서 공유하는 상태에는 클래스 설정을 사용하도록 안내합니다. 테스트를 독립적으로 재현해야 한다면 변경 가능한 난수 스트림은 테스트별 초기화에 둬야 합니다. 충돌 후에는 해체가 보장되지 않으므로 다음 테스트의 상태를 해체 단계에서 준비하는 것은 신뢰할 수 없습니다. 테스트에서 상태 설정 및 해체하기
Xcode의 예약과 입력 재현을 분리하기
Xcode에서 이 패키지를 실행하려면 Package.swift를 엽니다. 테스트 계획을 사용하는 프로젝트에서는 Product > Scheme > Edit Test Plan을 선택하고 관련 구성을 선택한 다음 Environment Variables 아래에 TEST_SEED을 십진수 값 42로 추가합니다. Product > Test로 실행하고 테스트 출력에 표시된 시드를 확인하세요. Apple 문서에서는 테스트 계획의 환경 설정과 테스트 실행 전 활성 계획 선택 방법을 설명합니다. 테스트 계획으로 테스트를 구성해 코드 평가 개선하기
터미널과 Xcode를 오갈 때마다 출력된 시드를 확인하세요. 단일 터미널 명령에 지정한 변수는 Xcode 테스트 계획 설정이 아닙니다. 로그 줄을 보면 테스트 프로세스가 실제로 어떤 구성을 읽었는지 알 수 있습니다.
Swift Testing은 기본적으로 테스트를 병렬 실행합니다. .serialized 특성은 매개변수화된 테스트 내 사례나 스위트 내 테스트를 직렬화할 수 있지만 관련 없는 테스트에는 적용되지 않습니다. Apple은 병렬화를 전역으로 비활성화하는 swift test --no-parallel도 문서화하고 있습니다. 두 제어 방식 모두 입력 시드를 제공하지는 않습니다. 각 사례가 자체 생성기와 숫자 입력을 사용하므로 이 예제에서는 직렬화가 필요하지 않습니다. 테스트를 직렬 또는 병렬로 실행하기
동일한 생성 입력으로 단독 실행 시에는 통과하지만 다른 테스트와 함께 실행할 때 실패한다면 공유 상태를 살펴보세요. 직렬 실행은 겹치는 작업을 조사하는 데 도움이 될 수 있지만, 시드만으로는 다른 테스트의 변경 가능한 객체를 초기화하거나 동시 실행 전체의 순서를 재현할 수 없습니다.
실패한 시드와 함께 무엇을 보존해야 할까요?
입력 시퀀스를 재구성하는 데 필요한 정보를 보존하세요.
- 테스트 이름과 십진수 시드
- 여기서 사용한 생성기 및 매핑 버전
upper10-v1 - 단언 메시지에 표시된 사례 인덱스와 구체적 입력
- 소스 리비전, Swift 버전, 선택된 Xcode 버전
- 입력 개수와 난수 추출 방식의 변경 사항
이 예제에서는 루프 전에 생성기 난수 추출을 하나 추가하면 이후 입력이 모두 이동합니다. 시프트 양을 변경하면 매핑이 바뀝니다. 입력 개수를 늘려도 생성기와 매핑이 그대로인 동안에만 기존 접두부가 유지됩니다. 이는 코드의 재현 규약에 따른 결과입니다.
일상적인 실행에는 작고 고정된 시드 컬렉션을 사용하고, 탐색할 때는 추가 명시적 시드를 시도하며, 결함을 찾으면 구체적인 회귀 입력을 저장하세요. 유용한 재현은 동일한 값과 알려진 초기 상태에서 시작합니다. 테스트가 시드를 사용하는 방식을 제어하기 때문에 시드를 신뢰할 수 있습니다.
출처
- SystemRandomNumberGenerator — Apple.
- 테스트 계획으로 테스트를 구성해 코드 평가 개선하기 — Apple.
- Random.swift — Swift Project.
- swift-package-init-lib.md — Swift Project.
- Swift 6 발표 — Swift.org.
- random(in:using:) — Apple.
- 매개변수화된 테스트 구현하기 — Apple.
- 테스트에서 상태 설정 및 해체하기 — Apple.
- 테스트를 직렬 또는 병렬로 실행하기 — Apple.