This content originally appeared on DEV Community and was authored by Yoshinori Imajo
đź§© Introduction
Sometimes in test code, we want to perform side effects just before a test case runs — without placing that setup logic directly inside the test function.
Swift Testing supports Arrange phases before a test, but it doesn’t (yet) allow asynchronous or side-effectful setup directly in @Test.
Sure, helper functions work — but what if we could express Arrange–Act–Assert (AAA) visibly and cleanly inside Swift Testing itself?
If you have suggestions for improvements, feel free to comment.
🎯 Goal
Here’s what we’d like to achieve:
// MARK: - Test case and its preparation
@Test(
// MARK: Arrange
// Ideally, asynchronous side effects could run here
)
func testCase1() async throws {
// MARK: Action
// MARK: Assertion
}
In short, this post shows that — yes, you can do this.
@Test already supports parameterized tests, so we can leverage that mechanism to perform async setup work.
đź’ˇ Motivation
You might think:
“You could already do this using TestTrait or TestScoping’s setUp().”
And yes — you’d be right. But I wanted per-test asynchronous setup, not a global one.
Example: testing a “read” operation might require writing data first. I want that Write to live right next to the test that reads it — not far away in a shared helper.
Of course, helper functions are an option, but they separate intent and context. I wanted my test’s Arrange to be visually and contextually adjacent to the test logic itself.
đź§© Example: Using Realm
Let’s look at a concrete example using Realm.
Realm is common in iOS apps, but it can behave unexpectedly across threads.
Testing concurrency-heavy code with Realm requires careful context handling.
In particular, the in-memory configuration is sensitive to scope.
Here’s a test that demonstrates how to perform async setup directly via @Test:
@Test(MyTestTrait(
actions: [ // Perform pre-test side effects here
{ realm in
print("Arrange Step 1:", realm, Thread.current)
},
{ realm in
// Another example — not always necessary
print("Arrange Step 2:", realm, Thread.current)
}
]
))
func example() async throws {
guard let realm = RealmContext.current?.realm else {
XCTFail("Realm not injected")
return
}
// Perform Realm tests
print("đź§Ş Using realm:", realm, Thread.current)
}
đź§© TaskLocal Context
We can define a TaskLocal context to safely inject the Realm instance:
struct RealmContext {
@TaskLocal static var current: RealmContext?
let realm: RLMRealm
}
This allows RealmContext.current to be available anywhere inside the async test body.
⚙️ Implementing the Trait
Here’s a minimal MyTestTrait implementation that performs setup and cleanup around the test body:
struct MyTestTrait {
// These actions are passed in from the test
let actions: [@Sendable (RLMRealm) async throws -> ()]
}
extension MyTestTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> ()
) async throws {
// Setup Realm (in-memory)
let realm = {
let config = RLMRealmConfiguration.default()
config.inMemoryIdentifier = UUID().uuidString
RLMRealmConfiguration.setDefault(config)
return RLMRealm.default()
// NOTE: This modifies the global .default configuration.
// It can leak into other tests, so use with caution.
}()
// Execute Arrange steps sequentially
for action in actions {
try await action(realm)
}
// Run the test body within the scoped context
try await RealmContext.$current.withValue(.init(realm: realm)) {
try await function()
}
}
}
This keeps setup logic close to your test definition,
while still leveraging Swift Testing’s structured concurrency and scoping.
đź§© Takeaways
- âś… You can use parameterized traits to perform async setup directly in
@Test - âś… TaskLocal makes dependency injection ergonomic and thread-safe
- ⚠️ Beware: changing Realm’s global configuration can leak between tests
- 🚫 localActions = actions–style “safety copies” are unnecessary; traits aren’t shared across tests
- đź§ Prefer direct initializers to redundant static factories
Notes
- Tested on Swift 6.1, Xcode 16.3
- RLMRealm is not Sendable — protect access carefully
- For projects using DI frameworks, this approach may overlap with existing patterns
- Yes, whether you should test side effects is a valid philosophical question
This content originally appeared on DEV Community and was authored by Yoshinori Imajo
Yoshinori Imajo | Sciencx (2025-10-23T07:55:56+00:00) Running Asynchronous Setup Side Effects in `@Test` with Swift Testing. Retrieved from https://www.scien.cx/2025/10/23/running-asynchronous-setup-side-effects-in-test-with-swift-testing-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.