opencodekit 0.16.15 → 0.16.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -242
- package/dist/index.js +19 -6
- package/dist/template/.opencode/AGENTS.md +72 -236
- package/dist/template/.opencode/README.md +49 -482
- package/dist/template/.opencode/agent/build.md +71 -345
- package/dist/template/.opencode/agent/explore.md +47 -139
- package/dist/template/.opencode/agent/general.md +61 -172
- package/dist/template/.opencode/agent/looker.md +65 -161
- package/dist/template/.opencode/agent/painter.md +46 -200
- package/dist/template/.opencode/agent/plan.md +37 -220
- package/dist/template/.opencode/agent/review.md +72 -153
- package/dist/template/.opencode/agent/scout.md +44 -486
- package/dist/template/.opencode/agent/vision.md +63 -178
- package/dist/template/.opencode/command/create.md +75 -307
- package/dist/template/.opencode/command/design.md +53 -589
- package/dist/template/.opencode/command/handoff.md +76 -180
- package/dist/template/.opencode/command/init.md +45 -211
- package/dist/template/.opencode/command/plan.md +62 -514
- package/dist/template/.opencode/command/pr.md +56 -226
- package/dist/template/.opencode/command/research.md +55 -266
- package/dist/template/.opencode/command/resume.md +33 -138
- package/dist/template/.opencode/command/review-codebase.md +54 -202
- package/dist/template/.opencode/command/ship.md +78 -127
- package/dist/template/.opencode/command/start.md +47 -577
- package/dist/template/.opencode/command/status.md +60 -353
- package/dist/template/.opencode/command/ui-review.md +52 -298
- package/dist/template/.opencode/command/verify.md +36 -250
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +133 -35
- package/dist/template/.opencode/plugin/README.md +40 -166
- package/dist/template/.opencode/plugin/compaction.ts +162 -131
- package/dist/template/.opencode/plugin/lib/memory-db.ts +112 -0
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +182 -27
- package/dist/template/.opencode/skill/augment-context-engine/SKILL.md +112 -0
- package/dist/template/.opencode/skill/augment-context-engine/mcp.json +6 -0
- package/dist/template/.opencode/skill/core-data-expert/SKILL.md +82 -0
- package/dist/template/.opencode/skill/core-data-expert/references/batch-operations.md +543 -0
- package/dist/template/.opencode/skill/core-data-expert/references/cloudkit-integration.md +259 -0
- package/dist/template/.opencode/skill/core-data-expert/references/concurrency.md +522 -0
- package/dist/template/.opencode/skill/core-data-expert/references/fetch-requests.md +643 -0
- package/dist/template/.opencode/skill/core-data-expert/references/glossary.md +233 -0
- package/dist/template/.opencode/skill/core-data-expert/references/migration.md +393 -0
- package/dist/template/.opencode/skill/core-data-expert/references/model-configuration.md +597 -0
- package/dist/template/.opencode/skill/core-data-expert/references/performance.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/persistent-history.md +553 -0
- package/dist/template/.opencode/skill/core-data-expert/references/project-audit.md +60 -0
- package/dist/template/.opencode/skill/core-data-expert/references/saving.md +574 -0
- package/dist/template/.opencode/skill/core-data-expert/references/stack-setup.md +625 -0
- package/dist/template/.opencode/skill/core-data-expert/references/testing.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/threading.md +589 -0
- package/dist/template/.opencode/skill/swift-concurrency/SKILL.md +246 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/actors.md +640 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-algorithms.md +822 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-await-basics.md +249 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-sequences.md +670 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/core-data.md +533 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/glossary.md +128 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/linting.md +142 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/memory-management.md +542 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/migration.md +1076 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/performance.md +574 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/sendable.md +578 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/tasks.md +604 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/testing.md +565 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/threading.md +452 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/SKILL.md +290 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-advanced.md +351 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/image-optimization.md +286 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/layout-best-practices.md +312 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/liquid-glass.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/list-patterns.md +153 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/modern-apis.md +400 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/performance-patterns.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/scroll-patterns.md +305 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/state-management.md +447 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/text-formatting.md +285 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/view-structure.md +276 -0
- package/dist/template/.opencode/tool/action-queue.ts +308 -0
- package/dist/template/.opencode/tool/swarm.ts +65 -40
- package/package.json +16 -3
- package/dist/template/.opencode/.agents/skills/context7/SKILL.md +0 -88
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# Testing Concurrent Code
|
|
2
|
+
|
|
3
|
+
Best practices for testing Swift Concurrency with Swift Testing (recommended) and XCTest.
|
|
4
|
+
|
|
5
|
+
## Recommendation: Use Swift Testing
|
|
6
|
+
|
|
7
|
+
**Swift Testing is strongly recommended** for new projects and tests. It provides:
|
|
8
|
+
- Modern Swift syntax with macros
|
|
9
|
+
- Better concurrency support
|
|
10
|
+
- Cleaner test structure
|
|
11
|
+
- More flexible test organization
|
|
12
|
+
|
|
13
|
+
XCTest patterns are included for legacy codebases.
|
|
14
|
+
|
|
15
|
+
## Swift Testing Basics
|
|
16
|
+
|
|
17
|
+
### Simple async test
|
|
18
|
+
|
|
19
|
+
```swift
|
|
20
|
+
@Test
|
|
21
|
+
@MainActor
|
|
22
|
+
func emptyQuery() async {
|
|
23
|
+
let searcher = ArticleSearcher()
|
|
24
|
+
await searcher.search("")
|
|
25
|
+
#expect(searcher.results == ArticleSearcher.allArticles)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Key differences from XCTest**:
|
|
30
|
+
- `@Test` macro instead of `XCTestCase`
|
|
31
|
+
- `#expect` instead of `XCTAssert`
|
|
32
|
+
- Structs preferred over classes
|
|
33
|
+
- No `test` prefix required
|
|
34
|
+
|
|
35
|
+
### Testing with actors
|
|
36
|
+
|
|
37
|
+
```swift
|
|
38
|
+
@Test
|
|
39
|
+
@MainActor
|
|
40
|
+
func searchReturnsResults() async {
|
|
41
|
+
let searcher = ArticleSearcher()
|
|
42
|
+
await searcher.search("swift")
|
|
43
|
+
#expect(!searcher.results.isEmpty)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Mark test with actor if system under test requires it.
|
|
48
|
+
|
|
49
|
+
> **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)
|
|
50
|
+
|
|
51
|
+
## Awaiting Async Callbacks
|
|
52
|
+
|
|
53
|
+
### Using continuations
|
|
54
|
+
|
|
55
|
+
When testing unstructured tasks:
|
|
56
|
+
|
|
57
|
+
```swift
|
|
58
|
+
@Test
|
|
59
|
+
@MainActor
|
|
60
|
+
func searchTaskCompletes() async {
|
|
61
|
+
let searcher = ArticleSearcher()
|
|
62
|
+
|
|
63
|
+
await withCheckedContinuation { continuation in
|
|
64
|
+
_ = withObservationTracking {
|
|
65
|
+
searcher.results
|
|
66
|
+
} onChange: {
|
|
67
|
+
continuation.resume()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
searcher.startSearchTask("swift")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#expect(searcher.results.count > 0)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Use when**: Testing code that spawns unstructured tasks.
|
|
78
|
+
|
|
79
|
+
### Using confirmations
|
|
80
|
+
|
|
81
|
+
For structured async code:
|
|
82
|
+
|
|
83
|
+
```swift
|
|
84
|
+
@Test
|
|
85
|
+
@MainActor
|
|
86
|
+
func searchTriggersObservation() async {
|
|
87
|
+
let searcher = ArticleSearcher()
|
|
88
|
+
|
|
89
|
+
await confirmation { confirm in
|
|
90
|
+
_ = withObservationTracking {
|
|
91
|
+
searcher.results
|
|
92
|
+
} onChange: {
|
|
93
|
+
confirm()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Must await here for confirmation to work
|
|
97
|
+
await searcher.search("swift")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#expect(!searcher.results.isEmpty)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Critical**: Must `await` async work for confirmation to validate.
|
|
105
|
+
|
|
106
|
+
## Setup and Teardown
|
|
107
|
+
|
|
108
|
+
### Using init/deinit
|
|
109
|
+
|
|
110
|
+
```swift
|
|
111
|
+
@MainActor
|
|
112
|
+
final class DatabaseTests {
|
|
113
|
+
let database: Database
|
|
114
|
+
|
|
115
|
+
init() async throws {
|
|
116
|
+
database = Database()
|
|
117
|
+
await database.prepare()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
deinit {
|
|
121
|
+
// Synchronous cleanup only
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
func insertsData() async throws {
|
|
126
|
+
try await database.insert(item)
|
|
127
|
+
#expect(await database.count() == 1)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Limitation**: `deinit` cannot call async methods.
|
|
133
|
+
|
|
134
|
+
### Test Scoping Traits
|
|
135
|
+
|
|
136
|
+
For async teardown:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
@MainActor
|
|
140
|
+
struct DatabaseTrait: SuiteTrait, TestTrait, TestScoping {
|
|
141
|
+
func provideScope(
|
|
142
|
+
for test: Test,
|
|
143
|
+
testCase: Test.Case?,
|
|
144
|
+
performing function: () async throws -> Void
|
|
145
|
+
) async throws {
|
|
146
|
+
let database = Database()
|
|
147
|
+
|
|
148
|
+
try await Environment.$database.withValue(database) {
|
|
149
|
+
await database.prepare()
|
|
150
|
+
try await function()
|
|
151
|
+
await database.cleanup() // Async teardown
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Environment for task-local storage
|
|
157
|
+
@MainActor
|
|
158
|
+
struct Environment {
|
|
159
|
+
@TaskLocal static var database = Database()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Apply to suite
|
|
163
|
+
@Suite(DatabaseTrait())
|
|
164
|
+
@MainActor
|
|
165
|
+
final class DatabaseTests {
|
|
166
|
+
@Test
|
|
167
|
+
func insertsData() async throws {
|
|
168
|
+
try await Environment.database.insert(item)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Or apply to individual test
|
|
173
|
+
@Test(DatabaseTrait())
|
|
174
|
+
func specificTest() async throws {
|
|
175
|
+
// Test code
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Use when**: Need async cleanup after each test.
|
|
180
|
+
|
|
181
|
+
## Handling Flaky Tests
|
|
182
|
+
|
|
183
|
+
### Problem: Race conditions
|
|
184
|
+
|
|
185
|
+
```swift
|
|
186
|
+
@Test
|
|
187
|
+
@MainActor
|
|
188
|
+
func isLoadingState() async throws {
|
|
189
|
+
let fetcher = ImageFetcher()
|
|
190
|
+
|
|
191
|
+
let task = Task { try await fetcher.fetch(url) }
|
|
192
|
+
|
|
193
|
+
// ❌ Flaky - may pass or fail
|
|
194
|
+
#expect(fetcher.isLoading == true)
|
|
195
|
+
|
|
196
|
+
try await task.value
|
|
197
|
+
#expect(fetcher.isLoading == false)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Issue**: Task may complete before we check `isLoading`.
|
|
202
|
+
|
|
203
|
+
### Solution: Swift Concurrency Extras
|
|
204
|
+
|
|
205
|
+
```swift
|
|
206
|
+
import ConcurrencyExtras
|
|
207
|
+
|
|
208
|
+
@Test
|
|
209
|
+
@MainActor
|
|
210
|
+
func isLoadingState() async throws {
|
|
211
|
+
try await withMainSerialExecutor {
|
|
212
|
+
let fetcher = ImageFetcher { url in
|
|
213
|
+
await Task.yield() // Allow test to check state
|
|
214
|
+
return Data()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let task = Task { try await fetcher.fetch(url) }
|
|
218
|
+
|
|
219
|
+
await Task.yield() // Switch to task
|
|
220
|
+
|
|
221
|
+
#expect(fetcher.isLoading == true) // ✅ Reliable
|
|
222
|
+
|
|
223
|
+
try await task.value
|
|
224
|
+
#expect(fetcher.isLoading == false)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Add package**: `https://github.com/pointfreeco/swift-concurrency-extras.git`
|
|
230
|
+
|
|
231
|
+
> **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)
|
|
232
|
+
|
|
233
|
+
### Serial execution required
|
|
234
|
+
|
|
235
|
+
```swift
|
|
236
|
+
@Suite(.serialized)
|
|
237
|
+
@MainActor
|
|
238
|
+
final class ImageFetcherTests {
|
|
239
|
+
// Tests run serially when using withMainSerialExecutor
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Critical**: Main serial executor doesn't work with parallel test execution.
|
|
244
|
+
|
|
245
|
+
## XCTest Patterns (Legacy)
|
|
246
|
+
|
|
247
|
+
### Basic async test
|
|
248
|
+
|
|
249
|
+
```swift
|
|
250
|
+
final class ArticleSearcherTests: XCTestCase {
|
|
251
|
+
@MainActor
|
|
252
|
+
func testEmptyQuery() async {
|
|
253
|
+
let searcher = ArticleSearcher()
|
|
254
|
+
await searcher.search("")
|
|
255
|
+
XCTAssertEqual(searcher.results, ArticleSearcher.allArticles)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Using expectations
|
|
261
|
+
|
|
262
|
+
```swift
|
|
263
|
+
@MainActor
|
|
264
|
+
func testSearchTask() async {
|
|
265
|
+
let searcher = ArticleSearcher()
|
|
266
|
+
let expectation = expectation(description: "Search complete")
|
|
267
|
+
|
|
268
|
+
_ = withObservationTracking {
|
|
269
|
+
searcher.results
|
|
270
|
+
} onChange: {
|
|
271
|
+
expectation.fulfill()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
searcher.startSearchTask("swift")
|
|
275
|
+
|
|
276
|
+
// Use fulfillment, not wait
|
|
277
|
+
await fulfillment(of: [expectation], timeout: 10)
|
|
278
|
+
|
|
279
|
+
XCTAssertEqual(searcher.results.count, 1)
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Critical**: Use `await fulfillment(of:)`, not `wait(for:)` to avoid deadlocks.
|
|
284
|
+
|
|
285
|
+
### Setup and teardown
|
|
286
|
+
|
|
287
|
+
```swift
|
|
288
|
+
final class DatabaseTests: XCTestCase {
|
|
289
|
+
override func setUp() async throws {
|
|
290
|
+
// Async setup
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
override func tearDown() async throws {
|
|
294
|
+
// Async teardown
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Mark as `async throws` to call async methods.
|
|
300
|
+
|
|
301
|
+
> **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)
|
|
302
|
+
|
|
303
|
+
### Main serial executor for all tests
|
|
304
|
+
|
|
305
|
+
```swift
|
|
306
|
+
final class MyTests: XCTestCase {
|
|
307
|
+
override func invokeTest() {
|
|
308
|
+
withMainSerialExecutor {
|
|
309
|
+
super.invokeTest()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Common Patterns
|
|
316
|
+
|
|
317
|
+
### Testing @MainActor code
|
|
318
|
+
|
|
319
|
+
```swift
|
|
320
|
+
@Test
|
|
321
|
+
@MainActor
|
|
322
|
+
func viewModelUpdates() async {
|
|
323
|
+
let viewModel = ViewModel()
|
|
324
|
+
await viewModel.loadData()
|
|
325
|
+
#expect(viewModel.items.count > 0)
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Testing actors
|
|
330
|
+
|
|
331
|
+
```swift
|
|
332
|
+
@Test
|
|
333
|
+
func actorIsolation() async {
|
|
334
|
+
let store = DataStore()
|
|
335
|
+
await store.insert(item)
|
|
336
|
+
let count = await store.count()
|
|
337
|
+
#expect(count == 1)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Testing cancellation
|
|
342
|
+
|
|
343
|
+
```swift
|
|
344
|
+
@Test
|
|
345
|
+
func cancellationStopsWork() async throws {
|
|
346
|
+
let processor = DataProcessor()
|
|
347
|
+
|
|
348
|
+
let task = Task {
|
|
349
|
+
try await processor.processLargeDataset()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
task.cancel()
|
|
353
|
+
|
|
354
|
+
do {
|
|
355
|
+
try await task.value
|
|
356
|
+
Issue.record("Should have thrown cancellation error")
|
|
357
|
+
} catch is CancellationError {
|
|
358
|
+
// Expected
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Testing with delays
|
|
364
|
+
|
|
365
|
+
```swift
|
|
366
|
+
@Test
|
|
367
|
+
func debouncedSearch() async throws {
|
|
368
|
+
try await withMainSerialExecutor {
|
|
369
|
+
let searcher = DebouncedSearcher()
|
|
370
|
+
|
|
371
|
+
searcher.search("a")
|
|
372
|
+
await Task.yield()
|
|
373
|
+
|
|
374
|
+
searcher.search("ab")
|
|
375
|
+
await Task.yield()
|
|
376
|
+
|
|
377
|
+
searcher.search("abc")
|
|
378
|
+
|
|
379
|
+
// Wait for debounce
|
|
380
|
+
try await Task.sleep(for: .milliseconds(600))
|
|
381
|
+
|
|
382
|
+
#expect(searcher.searchCount == 1) // Only last search executed
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Testing task groups
|
|
388
|
+
|
|
389
|
+
```swift
|
|
390
|
+
@Test
|
|
391
|
+
func taskGroupProcessesAll() async throws {
|
|
392
|
+
let processor = BatchProcessor()
|
|
393
|
+
|
|
394
|
+
let results = await withTaskGroup(of: Int.self) { group in
|
|
395
|
+
for i in 1...5 {
|
|
396
|
+
group.addTask { await processor.process(i) }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
var collected: [Int] = []
|
|
400
|
+
for await result in group {
|
|
401
|
+
collected.append(result)
|
|
402
|
+
}
|
|
403
|
+
return collected
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#expect(results.count == 5)
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Testing Memory Management
|
|
411
|
+
|
|
412
|
+
### Verify deallocation
|
|
413
|
+
|
|
414
|
+
```swift
|
|
415
|
+
@Test
|
|
416
|
+
func viewModelDeallocates() async {
|
|
417
|
+
var viewModel: ViewModel? = ViewModel()
|
|
418
|
+
weak var weakViewModel = viewModel
|
|
419
|
+
|
|
420
|
+
viewModel?.startWork()
|
|
421
|
+
viewModel = nil
|
|
422
|
+
|
|
423
|
+
try? await Task.sleep(for: .milliseconds(100))
|
|
424
|
+
|
|
425
|
+
#expect(weakViewModel == nil)
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Detect retain cycles
|
|
430
|
+
|
|
431
|
+
```swift
|
|
432
|
+
@Test
|
|
433
|
+
func noRetainCycle() async {
|
|
434
|
+
var manager: Manager? = Manager()
|
|
435
|
+
weak var weakManager = manager
|
|
436
|
+
|
|
437
|
+
manager?.startLongRunningTask()
|
|
438
|
+
manager = nil
|
|
439
|
+
|
|
440
|
+
#expect(weakManager == nil)
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Best Practices
|
|
445
|
+
|
|
446
|
+
1. **Use Swift Testing for new code** - modern, better concurrency support
|
|
447
|
+
2. **Mark tests with correct isolation** - @MainActor when needed
|
|
448
|
+
3. **Use confirmations over continuations** - when structured concurrency allows
|
|
449
|
+
4. **Serialize tests with main serial executor** - avoid flaky tests
|
|
450
|
+
5. **Test cancellation explicitly** - ensure proper cleanup
|
|
451
|
+
6. **Verify deallocation** - catch retain cycles early
|
|
452
|
+
7. **Use Task.yield() strategically** - control execution in tests
|
|
453
|
+
8. **Avoid sleep in tests** - use continuations/confirmations instead
|
|
454
|
+
9. **Test actor isolation** - verify thread safety
|
|
455
|
+
10. **Keep tests deterministic** - avoid timing dependencies
|
|
456
|
+
|
|
457
|
+
## Migration from XCTest
|
|
458
|
+
|
|
459
|
+
### XCTest → Swift Testing
|
|
460
|
+
|
|
461
|
+
```swift
|
|
462
|
+
// XCTest
|
|
463
|
+
final class MyTests: XCTestCase {
|
|
464
|
+
func testExample() async {
|
|
465
|
+
XCTAssertEqual(value, expected)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Swift Testing
|
|
470
|
+
@Suite
|
|
471
|
+
struct MyTests {
|
|
472
|
+
@Test
|
|
473
|
+
func example() async {
|
|
474
|
+
#expect(value == expected)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Expectations → Confirmations
|
|
480
|
+
|
|
481
|
+
```swift
|
|
482
|
+
// XCTest
|
|
483
|
+
let expectation = expectation(description: "Done")
|
|
484
|
+
doWork { expectation.fulfill() }
|
|
485
|
+
await fulfillment(of: [expectation])
|
|
486
|
+
|
|
487
|
+
// Swift Testing
|
|
488
|
+
await confirmation { confirm in
|
|
489
|
+
await doWork { confirm() }
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Setup/Teardown → Traits
|
|
494
|
+
|
|
495
|
+
```swift
|
|
496
|
+
// XCTest
|
|
497
|
+
override func setUp() async throws {
|
|
498
|
+
await prepare()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Swift Testing
|
|
502
|
+
struct SetupTrait: TestTrait, TestScoping {
|
|
503
|
+
func provideScope(
|
|
504
|
+
for test: Test,
|
|
505
|
+
testCase: Test.Case?,
|
|
506
|
+
performing function: () async throws -> Void
|
|
507
|
+
) async throws {
|
|
508
|
+
await prepare()
|
|
509
|
+
try await function()
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Troubleshooting
|
|
515
|
+
|
|
516
|
+
### Test hangs
|
|
517
|
+
|
|
518
|
+
**Cause**: Waiting for expectation that never fulfills.
|
|
519
|
+
|
|
520
|
+
**Solution**: Add timeout, verify observation tracking.
|
|
521
|
+
|
|
522
|
+
### Flaky test
|
|
523
|
+
|
|
524
|
+
**Cause**: Race condition in unstructured task.
|
|
525
|
+
|
|
526
|
+
**Solution**: Use main serial executor + Task.yield().
|
|
527
|
+
|
|
528
|
+
### Deadlock
|
|
529
|
+
|
|
530
|
+
**Cause**: Using `wait(for:)` in async context.
|
|
531
|
+
|
|
532
|
+
**Solution**: Use `await fulfillment(of:)` instead.
|
|
533
|
+
|
|
534
|
+
### Confirmation fails
|
|
535
|
+
|
|
536
|
+
**Cause**: Not awaiting async work in confirmation block.
|
|
537
|
+
|
|
538
|
+
**Solution**: Add `await` before async calls.
|
|
539
|
+
|
|
540
|
+
### Actor isolation error
|
|
541
|
+
|
|
542
|
+
**Cause**: Test not marked with required actor.
|
|
543
|
+
|
|
544
|
+
**Solution**: Add `@MainActor` or appropriate actor to test.
|
|
545
|
+
|
|
546
|
+
## Testing Checklist
|
|
547
|
+
|
|
548
|
+
- [ ] Tests marked with correct isolation
|
|
549
|
+
- [ ] Using Swift Testing (recommended)
|
|
550
|
+
- [ ] Async methods properly awaited
|
|
551
|
+
- [ ] Cancellation tested
|
|
552
|
+
- [ ] Memory leaks checked
|
|
553
|
+
- [ ] Race conditions handled
|
|
554
|
+
- [ ] Timeouts appropriate
|
|
555
|
+
- [ ] Flaky tests fixed with serial executor
|
|
556
|
+
- [ ] Actor isolation verified
|
|
557
|
+
- [ ] Cleanup in traits (not deinit)
|
|
558
|
+
|
|
559
|
+
## Further Learning
|
|
560
|
+
|
|
561
|
+
For advanced testing patterns, real-world examples, and migration strategies:
|
|
562
|
+
- [Swift Testing Documentation](https://developer.apple.com/documentation/testing)
|
|
563
|
+
- [Swift Concurrency Extras](https://github.com/pointfreeco/swift-concurrency-extras)
|
|
564
|
+
- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)
|
|
565
|
+
|