Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/testing.md
1# Testing Concurrent Code23Use this when:45- You are writing async tests.6- A test is flaky because of task scheduling or actor isolation.7- You need to replace XCTest waiting APIs or verify deallocation.89Skip this file if:1011- You mainly need production ownership guidance. Use `actors.md`, `tasks.md`, or `memory-management.md`.1213Jump to:1415- Swift Testing (Recommended)16- Awaiting Async Callbacks17- Setup and Teardown18- Handling Flaky Tests19- Swift Concurrency Extras20- XCTest Patterns (Legacy)21- Memory Management Tests22- Testing Checklist2324## Recommendation: Use Swift Testing2526**Swift Testing is strongly recommended** for new projects and tests. It provides:27- Modern Swift syntax with macros28- Better concurrency support29- Cleaner test structure30- More flexible test organization3132XCTest patterns are included for legacy codebases.3334## Swift Testing Basics3536### Simple async test3738```swift39@Test40@MainActor41func emptyQuery() async {42let searcher = ArticleSearcher()43await searcher.search("")44#expect(searcher.results == ArticleSearcher.allArticles)45}46```4748**Key differences from XCTest**:49- `@Test` macro instead of `XCTestCase`50- `#expect` instead of `XCTAssert`51- Structs preferred over classes52- No `test` prefix required5354### Testing with actors5556```swift57@Test58@MainActor59func searchReturnsResults() async {60let searcher = ArticleSearcher()61await searcher.search("swift")62#expect(!searcher.results.isEmpty)63}64```6566Mark test with actor if system under test requires it.6768> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.2: Testing concurrent code using Swift Testing](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)6970## Awaiting Async Callbacks7172### Using continuations7374When testing unstructured tasks:7576```swift77@Test78@MainActor79func searchTaskCompletes() async {80let searcher = ArticleSearcher()8182await withCheckedContinuation { continuation in83_ = withObservationTracking {84searcher.results85} onChange: {86continuation.resume()87}8889searcher.startSearchTask("swift")90}9192#expect(searcher.results.count > 0)93}94```9596**Use when**: Testing code that spawns unstructured tasks.9798### Using confirmations99100For structured async code:101102```swift103@Test104@MainActor105func searchTriggersObservation() async {106let searcher = ArticleSearcher()107108await confirmation { confirm in109_ = withObservationTracking {110searcher.results111} onChange: {112confirm()113}114115// Must await here for confirmation to work116await searcher.search("swift")117}118119#expect(!searcher.results.isEmpty)120}121```122123**Critical**: Must `await` async work for confirmation to validate.124125## Setup and Teardown126127### Using init/deinit128129```swift130@MainActor131final class DatabaseTests {132let database: Database133134init() async throws {135database = Database()136await database.prepare()137}138139deinit {140// Synchronous cleanup only141}142143@Test144func insertsData() async throws {145try await database.insert(item)146#expect(await database.count() == 1)147}148}149```150151**Limitation**: `deinit` cannot call async methods.152153### Test Scoping Traits154155For async teardown:156157```swift158@MainActor159struct DatabaseTrait: SuiteTrait, TestTrait, TestScoping {160func provideScope(161for test: Test,162testCase: Test.Case?,163performing function: () async throws -> Void164) async throws {165let database = Database()166167try await Environment.$database.withValue(database) {168await database.prepare()169try await function()170await database.cleanup() // Async teardown171}172}173}174175// Environment for task-local storage176@MainActor177struct Environment {178@TaskLocal static var database = Database()179}180181// Apply to suite182@Suite(DatabaseTrait())183@MainActor184final class DatabaseTests {185@Test186func insertsData() async throws {187try await Environment.database.insert(item)188}189}190191// Or apply to individual test192@Test(DatabaseTrait())193func specificTest() async throws {194// Test code195}196```197198**Use when**: Need async cleanup after each test.199200## Handling Flaky Tests201202### Problem: Race conditions203204```swift205@Test206@MainActor207func isLoadingState() async throws {208let fetcher = ImageFetcher()209210let task = Task { try await fetcher.fetch(url) }211212// ❌ Flaky - may pass or fail213#expect(fetcher.isLoading == true)214215try await task.value216#expect(fetcher.isLoading == false)217}218```219220**Issue**: Task may complete before we check `isLoading`.221222### Solution: Swift Concurrency Extras223224```swift225import ConcurrencyExtras226227@Test228@MainActor229func isLoadingState() async throws {230try await withMainSerialExecutor {231let fetcher = ImageFetcher { url in232await Task.yield() // Allow test to check state233return Data()234}235236let task = Task { try await fetcher.fetch(url) }237238await Task.yield() // Switch to task239240#expect(fetcher.isLoading == true) // ✅ Reliable241242try await task.value243#expect(fetcher.isLoading == false)244}245}246```247248**Add package**: `https://github.com/pointfreeco/swift-concurrency-extras.git`249250> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.3: Using Swift Concurrency Extras by Point-Free](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)251252### Serial execution required253254```swift255@Suite(.serialized)256@MainActor257final class ImageFetcherTests {258// Tests run serially when using withMainSerialExecutor259}260```261262**Critical**: Main serial executor doesn't work with parallel test execution.263264## XCTest Patterns (Legacy)265266### Basic async test267268```swift269final class ArticleSearcherTests: XCTestCase {270@MainActor271func testEmptyQuery() async {272let searcher = ArticleSearcher()273await searcher.search("")274XCTAssertEqual(searcher.results, ArticleSearcher.allArticles)275}276}277```278279### Using expectations280281```swift282@MainActor283func testSearchTask() async {284let searcher = ArticleSearcher()285let expectation = expectation(description: "Search complete")286287_ = withObservationTracking {288searcher.results289} onChange: {290expectation.fulfill()291}292293searcher.startSearchTask("swift")294295// Use fulfillment, not wait296await fulfillment(of: [expectation], timeout: 10)297298XCTAssertEqual(searcher.results.count, 1)299}300```301302**Critical**: Use `await fulfillment(of:)`, not `wait(for:)` to avoid deadlocks.303304### Setup and teardown305306```swift307final class DatabaseTests: XCTestCase {308override func setUp() async throws {309// Async setup310}311312override func tearDown() async throws {313// Async teardown314}315}316```317318Mark as `async throws` to call async methods.319320> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.1: Testing concurrent code using XCTest](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)321322### Main serial executor for all tests323324```swift325final class MyTests: XCTestCase {326override func invokeTest() {327withMainSerialExecutor {328super.invokeTest()329}330}331}332```333334## Common Patterns335336### Testing @MainActor code337338```swift339@Test340@MainActor341func viewModelUpdates() async {342let viewModel = ViewModel()343await viewModel.loadData()344#expect(viewModel.items.count > 0)345}346```347348### Testing actors349350```swift351@Test352func actorIsolation() async {353let store = DataStore()354await store.insert(item)355let count = await store.count()356#expect(count == 1)357}358```359360### Testing cancellation361362```swift363@Test364func cancellationStopsWork() async throws {365let processor = DataProcessor()366367let task = Task {368try await processor.processLargeDataset()369}370371task.cancel()372373do {374try await task.value375Issue.record("Should have thrown cancellation error")376} catch is CancellationError {377// Expected378}379}380```381382### Testing with delays383384```swift385@Test386func debouncedSearch() async throws {387try await withMainSerialExecutor {388let searcher = DebouncedSearcher()389390searcher.search("a")391await Task.yield()392393searcher.search("ab")394await Task.yield()395396searcher.search("abc")397398// Wait for debounce399try await Task.sleep(for: .milliseconds(600))400401#expect(searcher.searchCount == 1) // Only last search executed402}403}404```405406### Testing task groups407408```swift409@Test410func taskGroupProcessesAll() async throws {411let processor = BatchProcessor()412413let results = await withTaskGroup(of: Int.self) { group in414for i in 1...5 {415group.addTask { await processor.process(i) }416}417418var collected: [Int] = []419for await result in group {420collected.append(result)421}422return collected423}424425#expect(results.count == 5)426}427```428429## Testing Memory Management430431### Verify deallocation432433```swift434@Test435func viewModelDeallocates() async {436var viewModel: ViewModel? = ViewModel()437weak var weakViewModel = viewModel438439viewModel?.startWork()440viewModel = nil441442try? await Task.sleep(for: .milliseconds(100))443444#expect(weakViewModel == nil)445}446```447448### Detect retain cycles449450```swift451@Test452func noRetainCycle() async {453var manager: Manager? = Manager()454weak var weakManager = manager455456manager?.startLongRunningTask()457manager = nil458459#expect(weakManager == nil)460}461```462463## Best Practices4644651. **Use Swift Testing for new code** - modern, better concurrency support4662. **Mark tests with correct isolation** - @MainActor when needed4673. **Use confirmations over continuations** - when structured concurrency allows4684. **Serialize tests with main serial executor** - avoid flaky tests4695. **Test cancellation explicitly** - ensure proper cleanup4706. **Verify deallocation** - catch retain cycles early4717. **Use Task.yield() strategically** - control execution in tests4728. **Avoid sleep in tests** - use continuations/confirmations instead4739. **Test actor isolation** - verify thread safety47410. **Keep tests deterministic** - avoid timing dependencies475476## Migration from XCTest477478### XCTest → Swift Testing479480```swift481// XCTest482final class MyTests: XCTestCase {483func testExample() async {484XCTAssertEqual(value, expected)485}486}487488// Swift Testing489@Suite490struct MyTests {491@Test492func example() async {493#expect(value == expected)494}495}496```497498### Expectations → Confirmations499500```swift501// XCTest502let expectation = expectation(description: "Done")503doWork { expectation.fulfill() }504await fulfillment(of: [expectation])505506// Swift Testing507await confirmation { confirm in508await doWork { confirm() }509}510```511512### Setup/Teardown → Traits513514```swift515// XCTest516override func setUp() async throws {517await prepare()518}519520// Swift Testing521struct SetupTrait: TestTrait, TestScoping {522func provideScope(523for test: Test,524testCase: Test.Case?,525performing function: () async throws -> Void526) async throws {527await prepare()528try await function()529}530}531```532533## Troubleshooting534535### Test hangs536537**Cause**: Waiting for expectation that never fulfills.538539**Solution**: Add timeout, verify observation tracking.540541### Flaky test542543**Cause**: Race condition in unstructured task.544545**Solution**: Use main serial executor + Task.yield().546547### Deadlock548549**Cause**: Using `wait(for:)` in async context.550551**Solution**: Use `await fulfillment(of:)` instead.552553### Confirmation fails554555**Cause**: Not awaiting async work in confirmation block.556557**Solution**: Add `await` before async calls.558559### Actor isolation error560561**Cause**: Test not marked with required actor.562563**Solution**: Add `@MainActor` or appropriate actor to test.564565## Common Mistakes Agents Make566567- **Flaky intermediate-state assertions**: Asserting `isLoading == true` immediately after creating a `Task` is a race condition — the task may not have started yet. Use `withMainSerialExecutor` + `Task.yield()` to control scheduling before asserting intermediate state.568- **Using `Task.sleep` as a synchronization primitive** in tests instead of deterministic scheduling.569- **Asserting intermediate state without controlling scheduling**: Always use `withMainSerialExecutor` when you need to observe state between task creation and completion. Note: `withMainSerialExecutor` does not work with parallel test execution — mark the suite `@Suite(.serialized)`.570- **Reaching into isolated internals** instead of testing public behavior.571- **Keeping both Swift Testing and XCTest versions** of the same example unless they teach different migration paths.572573## Testing Checklist574575- [ ] Tests marked with correct isolation576- [ ] Using Swift Testing (recommended)577- [ ] Async methods properly awaited578- [ ] Cancellation tested579- [ ] Memory leaks checked580- [ ] Race conditions handled581- [ ] Timeouts appropriate582- [ ] Flaky tests fixed with serial executor583- [ ] Actor isolation verified584- [ ] Cleanup in traits (not deinit)585586## Further Learning587588For advanced testing patterns, real-world examples, and migration strategies:589- [Swift Testing Documentation](https://developer.apple.com/documentation/testing)590- [Swift Concurrency Extras](https://github.com/pointfreeco/swift-concurrency-extras)591- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)592593