Put shared asynchronous fixture preparation in a Swift Testing suite’s init() async throws. Swift Testing completes that initializer before calling an instance test method, so the method receives ready-to-use state. In XCTest, use setUp() async throws instead.
This guide is for a Mac developer preparing local unit-test data in Xcode. The task is to finish asynchronous initialization before assertions begin, using deterministic calculations with synthetic inputs. Apple documents the initializer approach in Migrating a test from XCTest.
How does Swift Testing wait for fixture initialization?
For an instance test method, Swift Testing initializes the containing suite type and then calls the method on that instance. The suite must have an initializer callable without arguments; that initializer may be asynchronous, throwing, or both. Each instance test method runs on a distinct instance. These are documented lifecycle rules in Organizing test functions with suite types.
The resulting dependency is straightforward: fixture preparation returns, the initializer finishes, and the test method starts. Your initializer must actually await the preparation operation. Merely starting work inside it does not establish that dependency.
Use the following decision aid to choose where preparation belongs:
| Situation | Preparation location | What establishes readiness? |
|---|---|---|
| Several instance tests need the same fixture recipe | Suite init() async throws |
The awaited initializer completes before each method |
| Only one test needs the fixture | An async throws test body |
An explicit try await before assertions |
An existing test subclasses XCTestCase |
setUp() async throws |
XCTest completes asynchronous setup before the method |
A Swift Testing test is static or global |
The test body or an explicitly called helper | The test explicitly awaits preparation |
This table applies the frameworks’ documented lifecycle rules to fixture placement. In particular, a suite initializer is a per-instance preparation point; it does not mean that preparation happens once for the entire suite. See Apple’s suite initialization rules and XCTest setup sequence.
Add a complete asynchronous fixture example in Xcode
Use Xcode 16 or later with its bundled Swift Testing support. The Swift project documents that Swift Testing is included with Swift 6 and Xcode 16, and that an additional package dependency is unnecessary. See the official Swift Testing README.
In an existing unit-test target, choose File > New > New File From Template, then select Swift Testing Unit Test. Replace the generated file contents with the complete example below. An existing XCTest unit-test bundle can also contain Swift Testing tests; a separate bundle is unnecessary. These steps follow Apple’s Adding tests to your Xcode project.
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)
}
}
The sample uses a real local actor to perform the calculation. Calling its isolated method from outside the actor requires await. There is no delay, random input, external dependency, or background operation that must happen to finish at the right moment. The output remains deterministic regardless of whether execution actually suspends.
The fixture factory rejects empty input, more than 100 values, and values outside 0...100. These illustrative limits keep the arithmetic small: each square is at most 10,000 and the total is at most 1,000,000. Invalid values produce an error rather than being silently removed or replaced.
For the supplied input, the expected calculation is 2² + 4² + 6² = 4 + 16 + 36 = 56. The initializer stores the completed fixture in a nonoptional let property. The test methods therefore need neither a readiness flag nor optional unwrapping.
The methods themselves can remain synchronous because their asynchronous prerequisite has already completed in the initializer. When asynchronous work belongs inside a test body instead, Apple’s documented pattern is to mark that function async and await the operation directly. See Testing asynchronous code.
The synthetic arithmetic was checked locally with Python. Swift is unavailable in this research environment, so the Swift examples were inspected but were not compiled or executed here. The expected results above describe the sample’s calculation, not a reported Xcode test run.
Why doesn’t starting a task make setup complete?
A common mistake is to launch Task { ... } from a synchronous initializer and return immediately. The initializer has then finished launching work, but that work may still be running when the test method starts.
Swift’s language guide describes Task { ... } as creating an unstructured task. Its returned handle can be used to wait for the result through await handle.value. Discarding the handle removes that explicit waiting point. See the official Swift book’s Unstructured Concurrency section.
For fixture initialization, directly awaiting the factory in an asynchronous initializer is usually the clearest expression of the dependency. If an existing helper launches work internally and returns before that work finishes, awaiting the helper only waits for its return. Change its completion contract so that returning means the fixture is ready, or expose a task result that the caller can await.
Adding a fixed sleep does not express that dependency. It waits for time to pass, leaving readiness dependent on whether preparation finishes within that interval. Likewise, making the test method async only enables asynchronous calls; the method must still await the operation it depends on.
What is the equivalent setup in XCTest?
XCTest provides a dedicated asynchronous setup hook. Its documented order before each test method is setUp() async throws, then setUpWithError(), then synchronous setUp(). The test method follows those hooks. See Set Up and Tear Down State in Your Tests.
To compare the same fixture under XCTest, add the following complete companion file to the same unit-test target as the first example. It imports XCTest and uses the previously defined, module-visible CalculationFixture. Keep the helper definitions in the first file; do not duplicate them.
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)
}
}
The XCTest method can also be synchronous: asynchronous preparation occurs in setup. The optional property accommodates XCTest’s separate setup phase, and XCTUnwrap makes a missing fixture an explicit failure. Apple documents that unwrapping pattern in its migration guide.
When converting this test to Swift Testing, move the preparation into the suite initializer and use import Testing, @Test, and Swift Testing expectations. A method merely named setUp in an ordinary Swift Testing suite is not the documented setup mechanism; Apple’s migration path replaces the setup family with initialization.
Check these boundaries when initialization still appears early
First, check whether the test is an instance method. A static test does not require a suite instance, so it cannot depend on automatic instance initialization. A global test has no containing instance either. For either form, construct and await the fixture in the test body. The supported test forms are described in Migrating a test from XCTest.
Second, inspect the full preparation call chain. Find the operation that actually produces the ready state and ensure each caller waits for it. In the example, the actor calculation finishes before the factory returns, and the factory returns before the initializer finishes. No work needed by the assertions escapes that sequence.
Third, let setup errors propagate. If CalculationFixture.make throws, the suite instance cannot finish initialization, so its instance test method cannot proceed on that instance. Avoid replacing failed preparation with an empty fixture through try? and a fallback unless that fallback is explicitly the behavior under test. The example’s invalid-input error deliberately prevents an apparently usable fixture from being created.
Finally, distinguish initialization order from test scheduling. Swift Testing runs tests in parallel by default. A suite’s .serialized trait can make its contained tests run serially, but serialization does not turn an unawaited task into awaited preparation, nor does it make the suite initializer run only once. Apple documents the trait’s scope in Running tests serially or in parallel.
For this local calculation, keep the fixture immutable and prepare it independently for each instance test. The essential check is the dependency chain: the preparation operation returns a finished fixture, the initializer awaits that result, and the test reads the initialized property. That gives the test a concrete readiness condition without timing assumptions.
Sources
- Organizing test functions with suite types — Apple
- Migrating a test from XCTest — Apple
- Set Up and Tear Down State in Your Tests — Apple
- Swift Testing README — Swift project
- Adding tests to your Xcode project — Apple
- Testing asynchronous code — Apple
- The Swift Programming Language: Concurrency — Swift project
- Running tests serially or in parallel — Apple