To replay randomized Swift test inputs on your Mac, pass an explicit seed into a fresh generator for each test case. Keep the generator, input mapping, and draw order fixed, then record the seed alongside every failure.
This walkthrough builds a small local Swift package that checks a synthetic arithmetic calculation. It provides a complete Swift Testing example and an XCTest alternative. All test data comes from local calculations; the package has no external dependencies.
What does the seed actually make repeatable?
There are two separate sources of variation: the values a test generates and the order in which the test runner schedules tests. An application-defined seed controls the first only when every generated value comes from the corresponding generator. Apple documents that ordinary random operations use the automatically seeded SystemRandomNumberGenerator by default. Its documented initializer accepts no seed. SystemRandomNumberGenerator
Xcode also offers randomized execution order to expose dependencies between tests. Its test-plan documentation describes alphabetical or random execution order, but does not document a user-supplied replay seed for that setting. The supported path used here is therefore an explicitly seeded input generator inside independent tests. It does not promise to reproduce the runner’s scheduling. Improving code assessment by organizing tests into test plans
Use this decision aid before changing a flaky test:
| What varies between runs? | What to control |
|---|---|
| Generated numeric inputs | Seed, generator implementation, input mapping, and draw order |
| Results after another test runs | Mutable state and per-test initialization |
| Results only during concurrent execution | Shared mutable resources and overlapping work |
| Inputs after a toolchain update | Input-mapping implementation and recorded concrete inputs |
Swift explicitly supports custom generators through RandomNumberGenerator, whose required operation is a mutating next() returning UInt64. The Swift project’s source documentation identifies repeatable sequences for testing as a reason to supply a seedable generator. Random.swift
Create a small local test package
Use a Mac with Xcode and an available Swift 6 or later toolchain. Check the selected command-line toolchain in Terminal, then create a new directory and initialize a library package:
swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library
The Swift project’s integration tests exercise library-package creation with swift package init --type library. Swift 6 includes Swift Testing directly, so this example needs no package dependency for import Testing. Swift Package Manager runs both Swift Testing tests and XCTest tests when present. Swift package initialization integration test, Announcing Swift 6
Keep the generated package manifest and library source. Replace the generated sample test file with the test files below, all inside Tests/SeedReplayTests. The arithmetic function lives in the test support file to keep this demonstration self-contained. In an existing project, use the real function under test instead.
Define the seed and input-generation contract
Create Tests/SeedReplayTests/ReplaySupport.swift with this complete content:
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, +)
}
The configuration accepts a decimal UInt64, including zero. An unset TEST_SEED selects three explicit seeds; an empty, negative, nonnumeric, or overflowing value throws an error. A mistyped seed therefore cannot silently select a different run.
The generator uses explicit wrapping arithmetic. Each generated input takes the upper ten bits of one output, giving values from 0 through 1023. That mapping is deliberately fixed in source. The arithmetic remains small: the largest expected result is 523776. This is a bounded synthetic input recipe, without a claim about statistical test coverage.
Why avoid Int.random(in:using:) here? That API supports a supplied generator and is useful when replaying with a fixed toolchain. However, Apple explicitly warns that its value-generation algorithm can change between Swift versions, even with a generator that produces the same underlying sequence. Owning this example’s small mapping removes that particular dependency. Int.random(in:using:)
Run and replay the Swift Testing version
Create 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])
}
The parameterized test makes each seed a separate test case. Apple documents that arguments: supplies collection elements to test invocations and permits throwing expressions. This keeps seed selection visible in the test model. The explicit loop index then identifies an individual generated input within that seed. Implementing parameterized tests
Run the default collection, then replay one seed twice:
swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test
These two seeded runs should generate the same numeric inputs with unchanged source. For seed 42, the first three inputs are 759, 163, and 285. The test checks 256 inputs for that seed. The separate stability test intentionally stays fixed at seed zero: it checks the generator and mapping regardless of the selected arithmetic-test seed.
The expected outcome of the supplied implementation is that all assertions pass. Swift was unavailable in the research environment, so the Swift files were inspected rather than compiled. An independent Python calculation checked all 768 arithmetic comparisons for the three default seeds and the displayed initial values; that check does not replace compiling the Swift package.
Practice replaying a deliberate calculation error
To see the diagnostic path, temporarily replace the return expression in triangularNumber(_:) with this labeled fragment:
// Temporary replacement inside triangularNumber(_:).
return n * n / 2
Run TEST_SEED=42 swift test again. The first generated input is 759. The altered expression yields 288040; the reference total is 288420. The assertion message identifies seed 42, case index 0, and input 759. The early return stops that invocation after its first recorded mismatch, keeping the diagnostic focused.
Restore return n * (n + 1) / 2 and rerun the same command. Keep the seed constant while evaluating the fix. Once a generated input has exposed a useful defect, preserve that concrete input in a named regression test as well; it remains understandable even if the generation recipe later changes.
Apply the same lifecycle to XCTest
For an XCTest-based target, keep ReplaySupport.swift and use the following complete test file instead of the Swift Testing file. Name it 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]
)
}
}
The same Terminal commands apply. Each call to generatedInputs(seed:) constructs a fresh generator, so no test consumes another test’s random stream. There is no generator state to clean up after the test.
If an existing XCTest fixture stores a generator as a property, reset it before every test method. Apple documents instance setup methods for per-test initialization and class setup for state shared across the class. A mutable random stream belongs in per-test initialization when tests must replay independently. Preparing the next test’s state in teardown is unreliable because teardown is not guaranteed after a crash. Set Up and Tear Down State in Your Tests
Keep Xcode scheduling separate from input replay
To run this package from Xcode, open its Package.swift. For a project that uses a test plan, choose Product > Scheme > Edit Test Plan, select the relevant configuration, and add TEST_SEED with the decimal value 42 under Environment Variables. Run with Product > Test, and confirm the printed seed in the test output. Apple documents test-plan environment configuration and selecting the active plan before running tests. Improving code assessment by organizing tests into test plans
Verify the printed seed whenever you switch between Terminal and Xcode. A variable assigned to a single Terminal command is not an Xcode test-plan setting. The log line tells you which configuration the test process actually read.
Swift Testing runs tests in parallel by default. Its .serialized trait can serialize cases within a parameterized test or tests within a suite, but it does not govern unrelated tests. Apple also documents swift test --no-parallel for globally disabling parallelization. Neither control supplies an input seed. This example needs no serialization because every case owns its generator and numeric inputs. Running tests serially or in parallel
If a test passes alone but fails alongside other tests despite identical generated inputs, examine shared state. Serial execution can help investigate overlapping work, but a seed cannot reset another test’s mutable objects or reproduce an entire concurrent schedule.
What should you keep with a failing seed?
Preserve enough information to reconstruct the input sequence:
- The test name and decimal seed.
- The generator and mapping version, here
upper10-v1. - The case index and concrete input from the assertion message.
- The source revision, Swift version, and selected Xcode version.
- The input count and any changes to how random draws are consumed.
In this example, adding a generator draw before the loop shifts every subsequent input. Changing the shift amount changes the mapping. Raising the input count preserves the existing prefix only while the generator and mapping remain unchanged. These are consequences of the code’s replay contract.
Keep a small fixed seed collection for routine runs, try additional explicit seeds when exploring, and save concrete regression inputs when they reveal a defect. A useful replay starts with the same values and a known initial state; the seed becomes reliable because the test controls how it is consumed.
Sources
- SystemRandomNumberGenerator — Apple.
- Improving code assessment by organizing tests into test plans — Apple.
- Random.swift — Swift Project.
- swift-package-init-lib.md — Swift Project.
- Announcing Swift 6 — Swift.org.
- random(in:using:) — Apple.
- Implementing parameterized tests — Apple.
- Set Up and Tear Down State in Your Tests — Apple.
- Running tests serially or in parallel — Apple.