engsys 1.0.0

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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/core/agents/aaron.md +152 -0
  4. package/core/agents/bert.md +115 -0
  5. package/core/agents/isabelle.md +136 -0
  6. package/core/agents/jody.md +150 -0
  7. package/core/agents/leith.md +111 -0
  8. package/core/agents/marcelo.md +282 -0
  9. package/core/agents/melvin.md +101 -0
  10. package/core/agents/nyx.md +152 -0
  11. package/core/agents/otto.md +168 -0
  12. package/core/agents/patricia.md +283 -0
  13. package/core/commands/design-audit-local.md +155 -0
  14. package/core/commands/design-audit.md +235 -0
  15. package/core/commands/design-critique.md +96 -0
  16. package/core/commands/file-issue.md +22 -0
  17. package/core/commands/generate-project.md +45 -0
  18. package/core/commands/implement-issue.md +37 -0
  19. package/core/commands/implement-project.md +40 -0
  20. package/core/commands/naturalize.md +61 -0
  21. package/core/commands/pre-push.md +29 -0
  22. package/core/commands/prep-review-collect.md +130 -0
  23. package/core/commands/prep-review-finalize.md +121 -0
  24. package/core/commands/prep-review-publish.md +113 -0
  25. package/core/commands/prep-review.md +65 -0
  26. package/core/commands/project-closeout.md +25 -0
  27. package/core/skills/agentic-eval/SKILL.md +195 -0
  28. package/core/skills/chrome-devtools/SKILL.md +97 -0
  29. package/core/skills/code-review/SKILL.md +26 -0
  30. package/core/skills/gh-cli/SKILL.md +2202 -0
  31. package/core/skills/git-commit/SKILL.md +124 -0
  32. package/core/skills/git-workflow-agents/SKILL.md +462 -0
  33. package/core/skills/git-workflow-agents/reference.md +220 -0
  34. package/core/skills/github-actions/SKILL.md +190 -0
  35. package/core/skills/github-issues/SKILL.md +154 -0
  36. package/core/skills/llm-structured-outputs/SKILL.md +323 -0
  37. package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
  38. package/core/skills/pre-push/SKILL.md +115 -0
  39. package/core/skills/refactor/SKILL.md +645 -0
  40. package/core/skills/web-design-reviewer/SKILL.md +371 -0
  41. package/core/skills/webapp-testing/SKILL.md +127 -0
  42. package/core/skills/webapp-testing/test-helper.js +56 -0
  43. package/core/templates/CLAUDE.md.tmpl +98 -0
  44. package/core/templates/adr-template.md +67 -0
  45. package/core/templates/gh-issue-templates/bug.md +39 -0
  46. package/core/templates/gh-issue-templates/content.md +42 -0
  47. package/core/templates/gh-issue-templates/enhancement.md +36 -0
  48. package/core/templates/gh-issue-templates/feature.md +39 -0
  49. package/core/templates/gh-issue-templates/infrastructure.md +41 -0
  50. package/core/templates/post-edit-reminders.sh.tmpl +19 -0
  51. package/core/templates/settings.json.tmpl +90 -0
  52. package/core/templates/settings.local.json.tmpl +3 -0
  53. package/core/workflows/agent-implementation-workflow.md +346 -0
  54. package/core/workflows/generate-project.md +258 -0
  55. package/core/workflows/implement-project-workflow.md +190 -0
  56. package/core/workflows/issue-tracking.md +89 -0
  57. package/core/workflows/project-closeout-ceremony.md +77 -0
  58. package/core/workflows/review-workflow.md +266 -0
  59. package/engsys.config.example.yaml +46 -0
  60. package/install +202 -0
  61. package/lessons-library/README.md +80 -0
  62. package/lessons-library/async-callbacks-verify-liveness.md +15 -0
  63. package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
  64. package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
  65. package/lessons-library/co-commit-entangled-work.md +15 -0
  66. package/lessons-library/dependabot-triage-playbook.md +17 -0
  67. package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
  68. package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
  69. package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
  70. package/lessons-library/iac-first-no-console-changes.md +15 -0
  71. package/lessons-library/independent-objective-review-gate.md +15 -0
  72. package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
  73. package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
  74. package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
  75. package/lessons-library/operator-choices-are-first-class.md +15 -0
  76. package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
  77. package/lessons-library/prove-causation-before-acting.md +15 -0
  78. package/lessons-library/re-read-state-before-acting.md +14 -0
  79. package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
  80. package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
  81. package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
  82. package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
  83. package/lessons-library/tests-can-assert-the-bug.md +15 -0
  84. package/lessons-library/verify-ground-truth-not-reports.md +15 -0
  85. package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
  86. package/lib/commands.js +356 -0
  87. package/lib/generate-team-avatars.mjs +251 -0
  88. package/lib/manifest.js +155 -0
  89. package/lib/render.js +135 -0
  90. package/lib/selftest.js +90 -0
  91. package/lib/util.js +89 -0
  92. package/lib/yaml.js +156 -0
  93. package/optional-agents/gary.md +86 -0
  94. package/optional-agents/jos.md +136 -0
  95. package/optional-agents/sandy.md +101 -0
  96. package/optional-agents/steve.md +161 -0
  97. package/package.json +43 -0
  98. package/stacks/cloud/aws/claude.fragment.md +17 -0
  99. package/stacks/cloud/aws/settings.fragment.json +39 -0
  100. package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
  101. package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
  102. package/stacks/cloud/azure/claude.fragment.md +17 -0
  103. package/stacks/cloud/azure/settings.fragment.json +45 -0
  104. package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
  105. package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
  106. package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
  107. package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
  108. package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
  109. package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
  110. package/stacks/cloud/gcp/claude.fragment.md +17 -0
  111. package/stacks/cloud/gcp/settings.fragment.json +40 -0
  112. package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
  113. package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
  114. package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
  115. package/stacks/db/prisma/claude.fragment.md +49 -0
  116. package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
  117. package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
  118. package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
  119. package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
  120. package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
  121. package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
  122. package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
  123. package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
  124. package/stacks/iac/bicep/claude.fragment.md +14 -0
  125. package/stacks/iac/bicep/settings.fragment.json +20 -0
  126. package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
  127. package/stacks/iac/cdk/claude.fragment.md +14 -0
  128. package/stacks/iac/cdk/settings.fragment.json +23 -0
  129. package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
  130. package/stacks/iac/terraform/claude.fragment.md +13 -0
  131. package/stacks/iac/terraform/settings.fragment.json +25 -0
  132. package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
  133. package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
  134. package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
  135. package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
  136. package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
  137. package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
  138. package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
  139. package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
  140. package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
  141. package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
  142. package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
  143. package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
  144. package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
  145. package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
  146. package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
  147. package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
  148. package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
  149. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
  150. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
  151. package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
  152. package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
  153. package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
  154. package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
  155. package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
  156. package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
  157. package/stacks/platform/android/claude.fragment.md +40 -0
  158. package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
  159. package/stacks/platform/android/settings.fragment.json +13 -0
  160. package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
  161. package/stacks/platform/ios/claude.fragment.md +24 -0
  162. package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
  163. package/stacks/platform/ios/settings.fragment.json +21 -0
  164. package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
  165. package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
  166. package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
  167. package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
  168. package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
  169. package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
  170. package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
  171. package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
  172. package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
  173. package/stacks/tooling/issue-tracker-linear/skills/issue-tracker-linear/SKILL.md +183 -0
@@ -0,0 +1,504 @@
1
+ # Swift Testing Patterns Reference
2
+
3
+ ## Contents
4
+ - Basic Tests and Traits
5
+ - Expectations and Requirements
6
+ - Suite Organization
7
+ - Parameterized Tests
8
+ - Confirmation and Known Issues
9
+ - Tags
10
+ - TestScoping and Test Organization
11
+ - Mocking and Test Doubles
12
+ - Testable Architecture
13
+ - Async and Concurrent Tests
14
+ - XCTest UI Tests — Page Object Pattern
15
+ - Performance Testing
16
+ - Snapshot Testing
17
+ - Test Attachments
18
+ - Exit Testing
19
+ - Test File Organization
20
+ - Common Mistakes and Review Checklist
21
+
22
+ ## Basic Tests and Traits
23
+
24
+ ```swift
25
+ import Testing
26
+
27
+ @Test("User can update their display name")
28
+ func updateDisplayName() {
29
+ var user = User(name: "Alice")
30
+ user.name = "Bob"
31
+ #expect(user.name == "Bob")
32
+ }
33
+
34
+ @Test(.tags(.validation, .email))
35
+ func validatesEmailFormat() { /* ... */ }
36
+ ```
37
+
38
+ ## Expectations and Requirements
39
+
40
+ ```swift
41
+ #expect(result == 42)
42
+ #expect(name.isEmpty == false)
43
+ #expect(items.count > 0, "Items should not be empty")
44
+
45
+ // Error type checking
46
+ #expect(throws: ValidationError.self) {
47
+ try validate(email: "not-an-email")
48
+ }
49
+
50
+ // Specific error matching
51
+ #expect {
52
+ try validate(email: "")
53
+ } throws: { error in
54
+ guard let err = error as? ValidationError else { return false }
55
+ return err == .empty
56
+ }
57
+
58
+ // #require unwraps or fails the test
59
+ let user = try #require(await fetchUser(id: 1))
60
+ let first = try #require(items.first)
61
+ ```
62
+
63
+ **Rule: Use `#require` when subsequent assertions depend on the value. Use `#expect` for independent checks.**
64
+
65
+ ## Suite Organization
66
+
67
+ ```swift
68
+ @Suite("User Authentication")
69
+ struct AuthTests {
70
+ let service: AuthService
71
+ let mockRepo: MockUserRepository
72
+
73
+ // init() replaces setUp() -- runs before each test
74
+ init() {
75
+ mockRepo = MockUserRepository()
76
+ service = AuthService(repository: mockRepo)
77
+ }
78
+
79
+ @Test func loginSucceeds() async throws {
80
+ let user = try await service.login(email: "test@test.com", password: "pass")
81
+ #expect(user.email == "test@test.com")
82
+ }
83
+
84
+ @Test func loginFailsWithBadPassword() async {
85
+ #expect(throws: AuthError.invalidCredentials) {
86
+ try await service.login(email: "test@test.com", password: "wrong")
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ Suites can nest for logical grouping:
93
+
94
+ ```swift
95
+ @Suite("Payments")
96
+ struct PaymentTests {
97
+ @Suite("Subscriptions")
98
+ struct SubscriptionTests {
99
+ @Test func renewsAutomatically() { /* ... */ }
100
+ }
101
+ @Suite("One-Time")
102
+ struct OneTimeTests {
103
+ @Test func chargesCorrectAmount() { /* ... */ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Parameterized Tests
109
+
110
+ ```swift
111
+ @Test("Email validation", arguments: [
112
+ ("user@example.com", true),
113
+ ("user@", false),
114
+ ("@example.com", false),
115
+ ("", false),
116
+ ])
117
+ func validateEmail(email: String, isValid: Bool) {
118
+ #expect(EmailValidator.isValid(email) == isValid)
119
+ }
120
+
121
+ // From CaseIterable
122
+ @Test(arguments: Currency.allCases)
123
+ func currencyHasSymbol(currency: Currency) {
124
+ #expect(currency.symbol.isEmpty == false)
125
+ }
126
+
127
+ // Two collections: cartesian product
128
+ @Test(arguments: [1, 2, 3], ["a", "b"])
129
+ func combinations(number: Int, letter: String) {
130
+ #expect(number > 0)
131
+ }
132
+
133
+ // Use zip for 1:1 pairing
134
+ @Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
135
+ func currencySymbols(code: String, symbol: String) {
136
+ #expect(Currency(code: code).symbol == symbol)
137
+ }
138
+ ```
139
+
140
+ Each argument combination runs as an independent test case reported separately.
141
+
142
+ ## Confirmation and Known Issues
143
+
144
+ ### Confirmation (Async Event Testing)
145
+
146
+ ```swift
147
+ // Basic confirmation -- event must fire exactly once
148
+ await confirmation("Received notification") { confirm in
149
+ let observer = NotificationCenter.default.addObserver(
150
+ forName: .userLoggedIn, object: nil, queue: .main
151
+ ) { _ in confirm() }
152
+ await authService.login()
153
+ NotificationCenter.default.removeObserver(observer)
154
+ }
155
+
156
+ // Expected count -- event must fire exactly N times
157
+ await confirmation("Received 3 items", expectedCount: 3) { confirm in
158
+ processor.onItem = { _ in confirm() }
159
+ await processor.process(items)
160
+ }
161
+ ```
162
+
163
+ ### Known Issues
164
+
165
+ ```swift
166
+ // Known failing test -- does not count as failure
167
+ withKnownIssue("Propane tank is empty") {
168
+ #expect(truck.grill.isHeating)
169
+ }
170
+
171
+ // Intermittent / flaky
172
+ withKnownIssue(isIntermittent: true) {
173
+ #expect(service.isReachable)
174
+ }
175
+
176
+ // Conditional
177
+ withKnownIssue {
178
+ #expect(foodTruck.grill.isHeating)
179
+ } when: {
180
+ !hasPropane
181
+ }
182
+
183
+ // Match specific issues only
184
+ try withKnownIssue {
185
+ let level = try #require(foodTruck.batteryLevel)
186
+ #expect(level >= 0.8)
187
+ } matching: { issue in
188
+ guard case .expectationFailed(let expectation) = issue.kind else { return false }
189
+ return expectation.isRequired
190
+ }
191
+ ```
192
+
193
+ If no known issues are recorded, Swift Testing records a distinct issue notifying you the problem may be resolved.
194
+
195
+ ## Tags
196
+
197
+ Tags must be declared as static members in an extension on `Tag`:
198
+
199
+ ```swift
200
+ extension Tag {
201
+ @Tag static var critical: Self
202
+ @Tag static var slow: Self
203
+ @Tag static var networking: Self
204
+ @Tag static var validation: Self
205
+ }
206
+
207
+ @Test(.tags(.critical, .networking))
208
+ func apiCallReturnsData() async throws { /* ... */ }
209
+ ```
210
+
211
+ Filter tests by tag in Xcode test plans or CLI (tag-based filtering syntax varies by toolchain — verify for your Swift version).
212
+
213
+ ## TestScoping and Test Organization
214
+
215
+ `TestScoping` consolidates per-test setup/teardown into reusable fixtures:
216
+
217
+ ```swift
218
+ struct DatabaseFixture: TestScoping {
219
+ let db: TestDatabase
220
+
221
+ func provideScope(
222
+ for test: Test, testCase: Test.Case?,
223
+ performing body: @Sendable () async throws -> Void
224
+ ) async throws {
225
+ let db = try await TestDatabase.create()
226
+ try await body()
227
+ try await db.destroy()
228
+ }
229
+ }
230
+
231
+ // Use with @Test trait
232
+ @Test(.tags(.database))
233
+ func insertsRecord() async throws {
234
+ // DatabaseFixture.provideScope wraps this test
235
+ }
236
+ ```
237
+
238
+ ## Mocking and Test Doubles
239
+
240
+ Define testable boundaries with protocols:
241
+
242
+ ```swift
243
+ protocol UserRepository: Sendable {
244
+ func fetch(id: String) async throws -> User
245
+ func save(_ user: User) async throws
246
+ }
247
+
248
+ struct MockUserRepository: UserRepository {
249
+ var users: [String: User] = [:]
250
+ var fetchError: Error?
251
+ var savedUsers: [User] = []
252
+
253
+ func fetch(id: String) async throws -> User {
254
+ if let error = fetchError { throw error }
255
+ guard let user = users[id] else { throw NotFoundError() }
256
+ return user
257
+ }
258
+
259
+ mutating func save(_ user: User) async throws {
260
+ savedUsers.append(user)
261
+ users[user.id] = user
262
+ }
263
+ }
264
+ ```
265
+
266
+ **Pattern:** Mocks conform to protocols, never subclass concrete types. Store call counts and arguments for verification.
267
+
268
+ ## Testable Architecture
269
+
270
+ Inject dependencies through initializers for testability:
271
+
272
+ ```swift
273
+ @Observable
274
+ class ProfileViewModel {
275
+ var user: User?
276
+ var error: Error?
277
+ private let repository: UserRepository
278
+
279
+ init(repository: UserRepository) {
280
+ self.repository = repository
281
+ }
282
+
283
+ func load() async {
284
+ do {
285
+ user = try await repository.fetch(id: "current")
286
+ } catch {
287
+ self.error = error
288
+ }
289
+ }
290
+ }
291
+
292
+ // Test with mock
293
+ @Test @MainActor func viewModelLoadsUser() async {
294
+ let mock = MockUserRepository(users: ["current": .preview])
295
+ let vm = ProfileViewModel(repository: mock)
296
+ await vm.load()
297
+ #expect(vm.user?.name == "Alice")
298
+ }
299
+
300
+ @Test @MainActor func viewModelHandlesError() async {
301
+ var mock = MockUserRepository()
302
+ mock.fetchError = URLError(.notConnectedToInternet)
303
+ let vm = ProfileViewModel(repository: mock)
304
+ await vm.load()
305
+ #expect(vm.user == nil)
306
+ #expect(vm.error != nil)
307
+ }
308
+ ```
309
+
310
+ ## Async and Concurrent Tests
311
+
312
+ ```swift
313
+ @Test @MainActor func viewModelUpdatesOnMainActor() async {
314
+ let vm = ProfileViewModel(repository: MockUserRepository())
315
+ await vm.load()
316
+ #expect(vm.user != nil)
317
+ }
318
+
319
+ // Clock injection for time-dependent logic
320
+ @Test func debounceUsesCorrectDelay() async throws {
321
+ let clock = TestClock()
322
+ let debouncer = Debouncer(delay: .seconds(1), clock: clock)
323
+ debouncer.submit { /* action */ }
324
+ await clock.advance(by: .milliseconds(500))
325
+ #expect(!debouncer.hasExecuted)
326
+ await clock.advance(by: .milliseconds(500))
327
+ #expect(debouncer.hasExecuted)
328
+ }
329
+
330
+ // Error path testing
331
+ @Test func fetchThrowsOnNetworkError() async {
332
+ var mock = MockUserRepository()
333
+ mock.fetchError = URLError(.notConnectedToInternet)
334
+ #expect(throws: URLError.self) {
335
+ try await mock.fetch(id: "1")
336
+ }
337
+ }
338
+ ```
339
+
340
+ ## XCTest UI Tests — Page Object Pattern
341
+
342
+ Swift Testing does not support UI testing. Use XCTest with XCUITest for all UI tests.
343
+
344
+ ```swift
345
+ class LoginUITests: XCTestCase {
346
+ let app = XCUIApplication()
347
+
348
+ override func setUpWithError() throws {
349
+ continueAfterFailure = false
350
+ app.launch()
351
+ }
352
+
353
+ func testLoginFlow() throws {
354
+ let loginPage = LoginPage(app: app)
355
+ let homePage = loginPage.login(email: "test@test.com", password: "password")
356
+ XCTAssertTrue(homePage.welcomeLabel.exists)
357
+ }
358
+ }
359
+ ```
360
+
361
+ ### Page Object Pattern
362
+
363
+ Encapsulate UI element queries in page objects for reusable, readable UI tests:
364
+
365
+ ```swift
366
+ struct LoginPage {
367
+ let app: XCUIApplication
368
+ var emailField: XCUIElement { app.textFields["Email"] }
369
+ var passwordField: XCUIElement { app.secureTextFields["Password"] }
370
+ var signInButton: XCUIElement { app.buttons["Sign In"] }
371
+
372
+ @discardableResult
373
+ func login(email: String, password: String) -> HomePage {
374
+ emailField.tap(); emailField.typeText(email)
375
+ passwordField.tap(); passwordField.typeText(password)
376
+ signInButton.tap()
377
+ return HomePage(app: app)
378
+ }
379
+ }
380
+
381
+ struct HomePage {
382
+ let app: XCUIApplication
383
+ var welcomeLabel: XCUIElement { app.staticTexts["Welcome"] }
384
+ }
385
+ ```
386
+
387
+ ## Performance Testing
388
+
389
+ ```swift
390
+ class FeedPerformanceTests: XCTestCase {
391
+ func testFeedParsingPerformance() throws {
392
+ let data = try loadFixture("large-feed.json")
393
+ let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
394
+ measure(metrics: metrics) {
395
+ _ = try? FeedParser.parse(data)
396
+ }
397
+ }
398
+ }
399
+ ```
400
+
401
+ Performance tests require XCTest — not available in Swift Testing.
402
+
403
+ ## Snapshot Testing
404
+
405
+ Use swift-snapshot-testing (pointfreeco) for visual regression. Requires XCTest:
406
+
407
+ ```swift
408
+ import SnapshotTesting
409
+ import XCTest
410
+
411
+ class ProfileViewSnapshotTests: XCTestCase {
412
+ func testProfileView() {
413
+ let view = ProfileView(user: .preview)
414
+ assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))
415
+
416
+ // Dark mode
417
+ assertSnapshot(of: view.environment(\.colorScheme, .dark),
418
+ as: .image(layout: .device(config: .iPhone13)), named: "dark")
419
+
420
+ // Large Dynamic Type
421
+ assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
422
+ as: .image(layout: .device(config: .iPhone13)), named: "largeText")
423
+ }
424
+ }
425
+ ```
426
+
427
+ Always test Dark Mode and large Dynamic Type in snapshots.
428
+
429
+ ## Test Attachments
430
+
431
+ Attach diagnostic data to test results for debugging failures:
432
+
433
+ ```swift
434
+ @Test func generateReport() async throws {
435
+ let report = try generateReport()
436
+ // Attach the output for later inspection
437
+ Attachment(report.data, named: "report.json").record()
438
+ #expect(report.isValid)
439
+ }
440
+
441
+ // Attach from a file URL
442
+ @Test func processImage() async throws {
443
+ let output = try processImage()
444
+ try await Attachment(contentsOf: output.url, named: "result.png")
445
+ .record()
446
+ }
447
+ ```
448
+
449
+ Attachments support any `Attachable` type and images via `AttachableAsImage`.
450
+
451
+ ## Exit Testing
452
+
453
+ Test code that calls `exit()`, `fatalError()`, or `preconditionFailure()`:
454
+
455
+ ```swift
456
+ @Test func invalidInputCausesExit() async {
457
+ await #expect(processExitsWith: .failure) {
458
+ processInvalidInput() // calls fatalError()
459
+ }
460
+ }
461
+ ```
462
+
463
+ Exit testing runs the closure in a subprocess. The test passes if the process exits with the expected status.
464
+
465
+ ## Test File Organization
466
+
467
+ ```text
468
+ Tests/AppTests/ # Swift Testing (Models/, ViewModels/, Services/)
469
+ Tests/AppUITests/ # XCTest UI tests (Pages/, Flows/)
470
+ Tests/Fixtures/ # Test data (JSON, images)
471
+ Tests/Mocks/ # Shared mock implementations
472
+ ```
473
+
474
+ Name test files `<TypeUnderTest>Tests.swift`. Describe behavior in function names: `fetchUserReturnsNilOnNetworkError()` not `testFetchUser()`. Name mocks `Mock<ProtocolName>`.
475
+
476
+ ### What to Test
477
+
478
+ **Always test:** business logic, validation rules, state transitions in view models, error handling paths, edge cases (empty collections, nil, boundaries), async success and failure, Task cancellation.
479
+
480
+ **Skip:** SwiftUI view body layout (use snapshots), simple property forwarding, Apple framework behavior, private methods (test through public API).
481
+
482
+ ## Common Mistakes and Review Checklist
483
+
484
+ 1. **Testing implementation, not behavior.** Test what the code does, not how.
485
+ 2. **No error path tests.** If a function can throw, test the throw path.
486
+ 3. **Flaky async tests.** Use `confirmation` with expected counts, not `sleep` calls.
487
+ 4. **Shared mutable state between tests.** Each test sets up its own state via `init()` in `@Suite`.
488
+ 5. **Missing accessibility identifiers in UI tests.** XCUITest queries rely on them.
489
+ 6. **Using `sleep` in tests.** Use `confirmation`, clock injection, or `withKnownIssue`.
490
+ 7. **Not testing cancellation.** If code supports `Task` cancellation, verify it cancels cleanly.
491
+ 8. **Mixing XCTest and Swift Testing in one file.** Keep them in separate files.
492
+ 9. **Non-Sendable test helpers shared across tests.** Ensure test helper types are Sendable when shared across concurrent test cases.
493
+
494
+ ### Review Checklist
495
+
496
+ - [ ] All new tests use Swift Testing (`@Test`, `#expect`), not XCTest assertions
497
+ - [ ] Test names describe behavior (`fetchUserReturnsNilOnNetworkError` not `testFetchUser`)
498
+ - [ ] Error paths have dedicated tests
499
+ - [ ] Async tests use `confirmation()`, not `Task.sleep`
500
+ - [ ] Parameterized tests used for repetitive variations
501
+ - [ ] Tags applied for filtering (`.critical`, `.slow`)
502
+ - [ ] Mocks conform to protocols, not subclass concrete types
503
+ - [ ] No shared mutable state between tests
504
+ - [ ] Cancellation tested for cancellable async operations