swift-code-reviewer-skill 1.2.0 → 1.3.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.
- package/CHANGELOG.md +43 -169
- package/README.md +43 -2
- package/SKILL.md +194 -711
- package/bin/install.js +1 -1
- package/package.json +2 -1
- package/references/agent-loop-feedback.md +148 -0
- package/references/companion-skills.md +70 -0
- package/references/spec-adherence.md +157 -0
- package/skills/README.md +43 -0
- package/skills/swift-concurrency/NOTICE.md +18 -0
- package/skills/swift-concurrency/SKILL.md +235 -0
- package/skills/swift-concurrency/references/actors.md +640 -0
- package/skills/swift-concurrency/references/async-await-basics.md +249 -0
- package/skills/swift-concurrency/references/async-sequences.md +635 -0
- package/skills/swift-concurrency/references/core-data.md +533 -0
- package/skills/swift-concurrency/references/glossary.md +96 -0
- package/skills/swift-concurrency/references/linting.md +38 -0
- package/skills/swift-concurrency/references/memory-management.md +542 -0
- package/skills/swift-concurrency/references/migration.md +721 -0
- package/skills/swift-concurrency/references/performance.md +574 -0
- package/skills/swift-concurrency/references/sendable.md +578 -0
- package/skills/swift-concurrency/references/tasks.md +604 -0
- package/skills/swift-concurrency/references/testing.md +565 -0
- package/skills/swift-concurrency/references/threading.md +452 -0
- package/skills/swift-expert/NOTICE.md +18 -0
- package/skills/swift-expert/SKILL.md +226 -0
- package/skills/swift-expert/references/async-concurrency.md +363 -0
- package/skills/swift-expert/references/memory-performance.md +380 -0
- package/skills/swift-expert/references/protocol-oriented.md +357 -0
- package/skills/swift-expert/references/swiftui-patterns.md +294 -0
- package/skills/swift-expert/references/testing-patterns.md +402 -0
- package/skills/swift-testing/NOTICE.md +18 -0
- package/skills/swift-testing/SKILL.md +295 -0
- package/skills/swift-testing/references/async-testing.md +245 -0
- package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
- package/skills/swift-testing/references/fixtures.md +193 -0
- package/skills/swift-testing/references/integration-testing.md +189 -0
- package/skills/swift-testing/references/migration-xctest.md +301 -0
- package/skills/swift-testing/references/parameterized-tests.md +171 -0
- package/skills/swift-testing/references/snapshot-testing.md +201 -0
- package/skills/swift-testing/references/test-doubles.md +243 -0
- package/skills/swift-testing/references/test-organization.md +231 -0
- package/skills/swiftui-expert-skill/NOTICE.md +18 -0
- package/skills/swiftui-expert-skill/SKILL.md +281 -0
- package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
- package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
- package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/skills/swiftui-expert-skill/references/state-management.md +417 -0
- package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
- package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
- package/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Dump Snapshot Testing
|
|
2
|
+
|
|
3
|
+
Dump snapshot testing captures text-based representations of data structures, perfect for testing models, state objects, and non-visual components.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Use [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing):
|
|
8
|
+
|
|
9
|
+
```swift
|
|
10
|
+
// Package.swift
|
|
11
|
+
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.0")
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
import SnapshotTesting
|
|
18
|
+
import Testing
|
|
19
|
+
@testable import Domain
|
|
20
|
+
|
|
21
|
+
@Suite("PersonalRecord Snapshots")
|
|
22
|
+
struct PersonalRecordSnapshotTests {
|
|
23
|
+
|
|
24
|
+
@Test("captures record structure correctly")
|
|
25
|
+
func recordStructure() {
|
|
26
|
+
let record = PersonalRecord.fixture(
|
|
27
|
+
liftType: .snatch,
|
|
28
|
+
weight: 120.0,
|
|
29
|
+
date: Date(timeIntervalSince1970: 1704067200) // Fixed date
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assertSnapshot(of: record, as: .dump)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## When to Use Dump Snapshots
|
|
38
|
+
|
|
39
|
+
| Use Case | Why Dump Snapshots |
|
|
40
|
+
|----------|-------------------|
|
|
41
|
+
| **Data models** | Verify all properties without writing assertions for each |
|
|
42
|
+
| **API responses** | Catch unexpected changes in decoded structures |
|
|
43
|
+
| **State objects** | Track complex state transitions |
|
|
44
|
+
| **Transformations** | Verify mapping/conversion logic output |
|
|
45
|
+
| **Configuration** | Ensure settings objects are correctly constructed |
|
|
46
|
+
|
|
47
|
+
## Parameterized Dump Snapshots
|
|
48
|
+
|
|
49
|
+
Test multiple configurations:
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
@Test("captures different lift types", arguments: LiftType.allCases)
|
|
53
|
+
func liftTypeSnapshots(liftType: LiftType) {
|
|
54
|
+
let record = PersonalRecord.fixture(
|
|
55
|
+
liftType: liftType,
|
|
56
|
+
weight: 100.0
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assertSnapshot(of: record, as: .dump, named: "\(liftType)")
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Complex Object Snapshots
|
|
64
|
+
|
|
65
|
+
```swift
|
|
66
|
+
@Test("captures workout session state")
|
|
67
|
+
func workoutSessionState() {
|
|
68
|
+
let session = WorkoutSession(
|
|
69
|
+
id: UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!,
|
|
70
|
+
exercises: [
|
|
71
|
+
Exercise.fixture(name: "Snatch", sets: 5, reps: 3),
|
|
72
|
+
Exercise.fixture(name: "Clean & Jerk", sets: 4, reps: 2)
|
|
73
|
+
],
|
|
74
|
+
startedAt: Date(timeIntervalSince1970: 1704067200),
|
|
75
|
+
status: .inProgress
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assertSnapshot(of: session, as: .dump)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Collections and Arrays
|
|
83
|
+
|
|
84
|
+
```swift
|
|
85
|
+
@Test("captures record history")
|
|
86
|
+
func recordHistory() {
|
|
87
|
+
let records = [
|
|
88
|
+
PersonalRecord.fixture(liftType: .snatch, weight: 100.0),
|
|
89
|
+
PersonalRecord.fixture(liftType: .snatch, weight: 105.0),
|
|
90
|
+
PersonalRecord.fixture(liftType: .snatch, weight: 110.0)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
assertSnapshot(of: records, as: .dump)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Nested Structures
|
|
98
|
+
|
|
99
|
+
```swift
|
|
100
|
+
@Test("captures user profile with nested data")
|
|
101
|
+
func userProfileSnapshot() {
|
|
102
|
+
let profile = UserProfile(
|
|
103
|
+
user: User.fixture(name: "Alice"),
|
|
104
|
+
settings: Settings.fixture(
|
|
105
|
+
notifications: true,
|
|
106
|
+
theme: .dark
|
|
107
|
+
),
|
|
108
|
+
recentRecords: [
|
|
109
|
+
PersonalRecord.fixture(liftType: .snatch)
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assertSnapshot(of: profile, as: .dump)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Comparing Dump vs Custom Dump
|
|
118
|
+
|
|
119
|
+
SnapshotTesting provides two text strategies:
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
// Standard Swift dump - uses Mirror API
|
|
123
|
+
assertSnapshot(of: object, as: .dump)
|
|
124
|
+
|
|
125
|
+
// Custom dump - more readable output (recommended)
|
|
126
|
+
assertSnapshot(of: object, as: .customDump)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Prefer `.customDump`** for better readability with:
|
|
130
|
+
- Sorted dictionary keys
|
|
131
|
+
- Condensed output for simple values
|
|
132
|
+
- Better enum representation
|
|
133
|
+
|
|
134
|
+
## Recording Mode
|
|
135
|
+
|
|
136
|
+
First run records baselines. To re-record:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
// Re-record this snapshot
|
|
140
|
+
assertSnapshot(of: record, as: .dump, record: true)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or use environment variable:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
SNAPSHOT_TESTING_RECORD=1 swift test
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Deterministic Snapshots
|
|
150
|
+
|
|
151
|
+
Ensure consistent output by controlling variable data:
|
|
152
|
+
|
|
153
|
+
```swift
|
|
154
|
+
@Test("captures record with deterministic values")
|
|
155
|
+
func deterministicSnapshot() {
|
|
156
|
+
// Use fixed UUID
|
|
157
|
+
let id = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
|
|
158
|
+
|
|
159
|
+
// Use fixed date
|
|
160
|
+
let date = Date(timeIntervalSince1970: 1704067200) // 2024-01-01
|
|
161
|
+
|
|
162
|
+
let record = PersonalRecord(
|
|
163
|
+
id: id,
|
|
164
|
+
liftType: .snatch,
|
|
165
|
+
weight: 120.0,
|
|
166
|
+
date: date
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assertSnapshot(of: record, as: .dump)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Organization
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Tests/
|
|
177
|
+
└── DomainTests/
|
|
178
|
+
├── Snapshots/
|
|
179
|
+
│ ├── PersonalRecordSnapshotTests.swift
|
|
180
|
+
│ └── __Snapshots__/
|
|
181
|
+
│ └── PersonalRecordSnapshotTests/
|
|
182
|
+
│ ├── recordStructure.txt
|
|
183
|
+
│ └── liftTypeSnapshots-snatch.txt
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Best Practices
|
|
187
|
+
|
|
188
|
+
### Use Fixtures with Fixed Values
|
|
189
|
+
|
|
190
|
+
```swift
|
|
191
|
+
// Good - deterministic
|
|
192
|
+
let record = PersonalRecord.fixture(
|
|
193
|
+
id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!,
|
|
194
|
+
date: Date(timeIntervalSince1970: 0)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Bad - non-deterministic
|
|
198
|
+
let record = PersonalRecord.fixture() // Random UUID, current date
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Name Parameterized Snapshots
|
|
202
|
+
|
|
203
|
+
```swift
|
|
204
|
+
// Good - clear file names
|
|
205
|
+
assertSnapshot(of: record, as: .dump, named: "snatch-120kg")
|
|
206
|
+
|
|
207
|
+
// Avoid - generic names
|
|
208
|
+
assertSnapshot(of: record, as: .dump)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Review Diffs Carefully
|
|
212
|
+
|
|
213
|
+
Dump snapshots capture all properties. When reviewing:
|
|
214
|
+
1. Verify intentional changes
|
|
215
|
+
2. Catch unintended side effects
|
|
216
|
+
3. Update baselines only after careful review
|
|
217
|
+
|
|
218
|
+
### Combine with Unit Tests
|
|
219
|
+
|
|
220
|
+
Dump snapshots complement, not replace, unit tests:
|
|
221
|
+
|
|
222
|
+
```swift
|
|
223
|
+
@Test("validates and snapshots transformation")
|
|
224
|
+
func transformRecord() {
|
|
225
|
+
let input = APIResponse.fixture()
|
|
226
|
+
let output = RecordMapper.map(input)
|
|
227
|
+
|
|
228
|
+
// Unit assertion for critical behavior
|
|
229
|
+
#expect(output.weight == input.weightKg)
|
|
230
|
+
|
|
231
|
+
// Snapshot for complete structure
|
|
232
|
+
assertSnapshot(of: output, as: .dump)
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Troubleshooting
|
|
237
|
+
|
|
238
|
+
### Non-Deterministic Failures
|
|
239
|
+
|
|
240
|
+
If snapshots fail intermittently:
|
|
241
|
+
- Check for `UUID()` or `Date()` without fixed values
|
|
242
|
+
- Ensure dictionary ordering is consistent
|
|
243
|
+
- Use `.customDump` for sorted keys
|
|
244
|
+
|
|
245
|
+
### Large Snapshots
|
|
246
|
+
|
|
247
|
+
For objects with many properties:
|
|
248
|
+
|
|
249
|
+
```swift
|
|
250
|
+
// Snapshot specific parts
|
|
251
|
+
assertSnapshot(of: session.exercises, as: .dump, named: "exercises")
|
|
252
|
+
assertSnapshot(of: session.metadata, as: .dump, named: "metadata")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Unreadable Output
|
|
256
|
+
|
|
257
|
+
Switch to custom dump for cleaner output:
|
|
258
|
+
|
|
259
|
+
```swift
|
|
260
|
+
// Before: standard dump
|
|
261
|
+
assertSnapshot(of: complex, as: .dump)
|
|
262
|
+
|
|
263
|
+
// After: cleaner formatting
|
|
264
|
+
assertSnapshot(of: complex, as: .customDump)
|
|
265
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Fixtures
|
|
2
|
+
|
|
3
|
+
Fixtures are factory methods that simplify creating test objects with sensible defaults.
|
|
4
|
+
|
|
5
|
+
## Fixture Placement
|
|
6
|
+
|
|
7
|
+
Place fixtures **close to the model**, not in test targets:
|
|
8
|
+
|
|
9
|
+
```swift
|
|
10
|
+
// In Sources/Models/PersonalRecord.swift
|
|
11
|
+
|
|
12
|
+
public struct PersonalRecord: Equatable, Sendable {
|
|
13
|
+
public let id: UUID
|
|
14
|
+
public let liftType: LiftType
|
|
15
|
+
public let weight: Double
|
|
16
|
+
public let reps: Int
|
|
17
|
+
public let date: Date
|
|
18
|
+
public let isPersonalBest: Bool
|
|
19
|
+
|
|
20
|
+
public init(
|
|
21
|
+
id: UUID,
|
|
22
|
+
liftType: LiftType,
|
|
23
|
+
weight: Double,
|
|
24
|
+
reps: Int,
|
|
25
|
+
date: Date,
|
|
26
|
+
isPersonalBest: Bool = false
|
|
27
|
+
) {
|
|
28
|
+
self.id = id
|
|
29
|
+
self.liftType = liftType
|
|
30
|
+
self.weight = weight
|
|
31
|
+
self.reps = reps
|
|
32
|
+
self.date = date
|
|
33
|
+
self.isPersonalBest = isPersonalBest
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fixture lives alongside the model
|
|
38
|
+
#if DEBUG
|
|
39
|
+
extension PersonalRecord {
|
|
40
|
+
public static func fixture(
|
|
41
|
+
id: UUID = UUID(),
|
|
42
|
+
liftType: LiftType = .snatch,
|
|
43
|
+
weight: Double = 100.0,
|
|
44
|
+
reps: Int = 1,
|
|
45
|
+
date: Date = Date(),
|
|
46
|
+
isPersonalBest: Bool = false
|
|
47
|
+
) -> PersonalRecord {
|
|
48
|
+
PersonalRecord(
|
|
49
|
+
id: id,
|
|
50
|
+
liftType: liftType,
|
|
51
|
+
weight: weight,
|
|
52
|
+
reps: reps,
|
|
53
|
+
date: date,
|
|
54
|
+
isPersonalBest: isPersonalBest
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
#endif
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Benefits
|
|
62
|
+
|
|
63
|
+
1. **Tests show relevant data**: Only specify properties that matter
|
|
64
|
+
2. **Reduces boilerplate**: Defaults for unimportant properties
|
|
65
|
+
3. **Consistent test data**: Same defaults across suite
|
|
66
|
+
4. **Auto-available**: No imports beyond model's module
|
|
67
|
+
5. **Zero production overhead**: `#if DEBUG` strips from release
|
|
68
|
+
|
|
69
|
+
## Usage Patterns
|
|
70
|
+
|
|
71
|
+
### Minimal Specification
|
|
72
|
+
|
|
73
|
+
```swift
|
|
74
|
+
@Test("returns nickname when present")
|
|
75
|
+
func returnsNicknameWhenPresent() {
|
|
76
|
+
// Only specify what matters for THIS test
|
|
77
|
+
let user = User.fixture(nickname: "Johnny")
|
|
78
|
+
let sut = ProfileViewModel(user: user)
|
|
79
|
+
|
|
80
|
+
let displayName = sut.getUserName()
|
|
81
|
+
|
|
82
|
+
#expect(displayName == "Johnny")
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multiple Fixtures
|
|
87
|
+
|
|
88
|
+
```swift
|
|
89
|
+
@Test("sorts records by date")
|
|
90
|
+
func sortsRecordsByDate() {
|
|
91
|
+
let oldRecord = PersonalRecord.fixture(
|
|
92
|
+
date: Date().addingTimeInterval(-86400)
|
|
93
|
+
)
|
|
94
|
+
let newRecord = PersonalRecord.fixture(
|
|
95
|
+
date: Date()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
let sorted = sut.sort([oldRecord, newRecord])
|
|
99
|
+
|
|
100
|
+
#expect(sorted.first?.id == newRecord.id)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Fixture Collections
|
|
105
|
+
|
|
106
|
+
```swift
|
|
107
|
+
#if DEBUG
|
|
108
|
+
extension PersonalRecord {
|
|
109
|
+
public static func fixtures(count: Int) -> [PersonalRecord] {
|
|
110
|
+
(0..<count).map { _ in .fixture() }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static var sampleCollection: [PersonalRecord] {
|
|
114
|
+
[
|
|
115
|
+
.fixture(liftType: .snatch, weight: 80),
|
|
116
|
+
.fixture(liftType: .cleanAndJerk, weight: 100),
|
|
117
|
+
.fixture(liftType: .squat, weight: 150),
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
#endif
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Nested Fixtures
|
|
125
|
+
|
|
126
|
+
```swift
|
|
127
|
+
#if DEBUG
|
|
128
|
+
extension User {
|
|
129
|
+
public static func fixture(
|
|
130
|
+
id: UUID = UUID(),
|
|
131
|
+
profile: Profile = .fixture(),
|
|
132
|
+
settings: Settings = .fixture()
|
|
133
|
+
) -> User {
|
|
134
|
+
User(id: id, profile: profile, settings: settings)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
extension Profile {
|
|
139
|
+
public static func fixture(
|
|
140
|
+
name: String = "Test User",
|
|
141
|
+
email: String = "test@example.com"
|
|
142
|
+
) -> Profile {
|
|
143
|
+
Profile(name: name, email: email)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
#endif
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Fixture Guidelines
|
|
150
|
+
|
|
151
|
+
### Do
|
|
152
|
+
|
|
153
|
+
- Provide sensible defaults for all properties
|
|
154
|
+
- Make defaults representative of typical data
|
|
155
|
+
- Use `#if DEBUG` to exclude from production
|
|
156
|
+
- Make fixture method `public static`
|
|
157
|
+
- Mirror initializer parameter order
|
|
158
|
+
|
|
159
|
+
### Don't
|
|
160
|
+
|
|
161
|
+
- Use random values (breaks repeatability)
|
|
162
|
+
- Include fixtures in production builds
|
|
163
|
+
- Create fixtures in test targets (harder to share)
|
|
164
|
+
- Use dates like `Date()` without allowing override
|
|
165
|
+
|
|
166
|
+
## Date Handling
|
|
167
|
+
|
|
168
|
+
```swift
|
|
169
|
+
#if DEBUG
|
|
170
|
+
extension PersonalRecord {
|
|
171
|
+
public static func fixture(
|
|
172
|
+
// Use a fixed reference date, not Date()
|
|
173
|
+
date: Date = Date(timeIntervalSince1970: 1704067200) // 2024-01-01
|
|
174
|
+
) -> PersonalRecord {
|
|
175
|
+
// ...
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
#endif
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Or use a test clock dependency:
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
@Dependency(\.date) var date
|
|
185
|
+
|
|
186
|
+
// In test
|
|
187
|
+
let fixedDate = Date(timeIntervalSince1970: 1704067200)
|
|
188
|
+
withDependencies {
|
|
189
|
+
$0.date = .constant(fixedDate)
|
|
190
|
+
} operation: {
|
|
191
|
+
// Tests use fixed date
|
|
192
|
+
}
|
|
193
|
+
```
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Integration Testing
|
|
2
|
+
|
|
3
|
+
Integration tests verify module interactions - the 15% of your testing pyramid.
|
|
4
|
+
|
|
5
|
+
## When to Write Integration Tests
|
|
6
|
+
|
|
7
|
+
- Testing component boundaries (use case -> repository -> storage)
|
|
8
|
+
- Verifying workflows across multiple components
|
|
9
|
+
- Testing real implementations with in-memory storage
|
|
10
|
+
- Validating data flows end-to-end within a module
|
|
11
|
+
|
|
12
|
+
## Basic Structure
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import Testing
|
|
16
|
+
@testable import PersonalRecordsCore
|
|
17
|
+
|
|
18
|
+
@Suite("PersonalRecords Integration Tests")
|
|
19
|
+
struct PersonalRecordsIntegrationTests {
|
|
20
|
+
|
|
21
|
+
@Test("save and retrieve workflow completes successfully")
|
|
22
|
+
func saveAndRetrieveWorkflow() async throws {
|
|
23
|
+
// Use real implementations with in-memory storage
|
|
24
|
+
let storage = InMemoryStorageService()
|
|
25
|
+
let repository = PersonalRecordsRepository(storage: storage)
|
|
26
|
+
let saveUseCase = SavePRUseCase(repository: repository)
|
|
27
|
+
let loadUseCase = LoadPRUseCase(repository: repository)
|
|
28
|
+
|
|
29
|
+
let record = PersonalRecord.fixture(weight: 120.0)
|
|
30
|
+
|
|
31
|
+
// Save
|
|
32
|
+
try await saveUseCase.dispatch(record)
|
|
33
|
+
|
|
34
|
+
// Retrieve and verify
|
|
35
|
+
let loaded = try await loadUseCase.dispatch()
|
|
36
|
+
|
|
37
|
+
#expect(loaded.count == 1)
|
|
38
|
+
#expect(loaded.first?.weight == 120.0)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## In-Memory Implementations
|
|
44
|
+
|
|
45
|
+
Create fakes for external dependencies:
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
final class InMemoryStorageService: StorageServiceProtocol {
|
|
49
|
+
private var storage: [String: Data] = [:]
|
|
50
|
+
private let lock = NSLock()
|
|
51
|
+
|
|
52
|
+
func save(_ data: Data, forKey key: String) async throws {
|
|
53
|
+
lock.lock()
|
|
54
|
+
defer { lock.unlock() }
|
|
55
|
+
storage[key] = data
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func load(forKey key: String) async throws -> Data? {
|
|
59
|
+
lock.lock()
|
|
60
|
+
defer { lock.unlock() }
|
|
61
|
+
return storage[key]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func delete(forKey key: String) async throws {
|
|
65
|
+
lock.lock()
|
|
66
|
+
defer { lock.unlock() }
|
|
67
|
+
storage.removeValue(forKey: key)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Testing Workflows
|
|
73
|
+
|
|
74
|
+
### Multi-Step Operations
|
|
75
|
+
|
|
76
|
+
```swift
|
|
77
|
+
@Test("complete user registration workflow")
|
|
78
|
+
func registrationWorkflow() async throws {
|
|
79
|
+
// Setup real components with test dependencies
|
|
80
|
+
let userStorage = InMemoryUserStorage()
|
|
81
|
+
let tokenStorage = InMemoryTokenStorage()
|
|
82
|
+
let userService = UserService(storage: userStorage)
|
|
83
|
+
let authService = AuthService(tokenStorage: tokenStorage)
|
|
84
|
+
|
|
85
|
+
let sut = RegistrationWorker(
|
|
86
|
+
userService: userService,
|
|
87
|
+
authService: authService
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Execute workflow
|
|
91
|
+
let result = try await sut.register(
|
|
92
|
+
username: "testuser",
|
|
93
|
+
password: "SecurePass123"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Verify end state
|
|
97
|
+
#expect(result.isSuccess)
|
|
98
|
+
#expect(userStorage.users.contains { $0.username == "testuser" })
|
|
99
|
+
#expect(tokenStorage.hasToken)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Error Propagation
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
@Test("propagates storage errors through use case")
|
|
107
|
+
func errorPropagation() async throws {
|
|
108
|
+
let failingStorage = FailingStorageService()
|
|
109
|
+
let repository = PersonalRecordsRepository(storage: failingStorage)
|
|
110
|
+
let sut = LoadPRUseCase(repository: repository)
|
|
111
|
+
|
|
112
|
+
#expect(throws: PRError.storageUnavailable) {
|
|
113
|
+
try await sut.dispatch()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Tagging Integration Tests
|
|
119
|
+
|
|
120
|
+
Use tags to filter tests:
|
|
121
|
+
|
|
122
|
+
```swift
|
|
123
|
+
extension Tag {
|
|
124
|
+
@Tag static var integration: Self
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Suite("Integration Tests", .tags(.integration))
|
|
128
|
+
struct PersonalRecordsIntegrationTests {
|
|
129
|
+
// ...
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Run only integration tests:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
swift test --filter integration
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Integration Test Guidelines
|
|
140
|
+
|
|
141
|
+
### Do
|
|
142
|
+
|
|
143
|
+
- Test component boundaries
|
|
144
|
+
- Use in-memory implementations for storage
|
|
145
|
+
- Test complete workflows
|
|
146
|
+
- Verify data flows correctly
|
|
147
|
+
- Tag tests for filtering
|
|
148
|
+
|
|
149
|
+
### Don't
|
|
150
|
+
|
|
151
|
+
- Test UI (use snapshot/UI tests)
|
|
152
|
+
- Use real network calls
|
|
153
|
+
- Use real databases
|
|
154
|
+
- Test third-party libraries
|
|
155
|
+
- Write too many (15% of pyramid)
|
|
156
|
+
|
|
157
|
+
## Test Organization
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Tests/
|
|
161
|
+
└── PersonalRecordsCoreTests/
|
|
162
|
+
├── Unit/
|
|
163
|
+
│ ├── UseCases/
|
|
164
|
+
│ └── Repositories/
|
|
165
|
+
├── Integration/
|
|
166
|
+
│ ├── WorkflowTests.swift
|
|
167
|
+
│ └── DataFlowTests.swift
|
|
168
|
+
└── Helpers/
|
|
169
|
+
└── InMemoryStorage.swift
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Performance Considerations
|
|
173
|
+
|
|
174
|
+
Integration tests are slower than unit tests:
|
|
175
|
+
|
|
176
|
+
```swift
|
|
177
|
+
@Test("bulk import performance", .timeLimit(.minutes(1)))
|
|
178
|
+
func bulkImportPerformance() async throws {
|
|
179
|
+
let storage = InMemoryStorageService()
|
|
180
|
+
let repository = PersonalRecordsRepository(storage: storage)
|
|
181
|
+
let sut = BulkImportUseCase(repository: repository)
|
|
182
|
+
|
|
183
|
+
let records = PersonalRecord.fixtures(count: 1000)
|
|
184
|
+
|
|
185
|
+
try await sut.dispatch(records)
|
|
186
|
+
|
|
187
|
+
#expect(storage.count == 1000)
|
|
188
|
+
}
|
|
189
|
+
```
|