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,301 @@
|
|
|
1
|
+
# Migration from XCTest to Swift Testing
|
|
2
|
+
|
|
3
|
+
How to migrate existing XCTest tests to Swift Testing.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
| XCTest | Swift Testing |
|
|
8
|
+
|--------|---------------|
|
|
9
|
+
| `class FooTests: XCTestCase` | `@Suite struct FooTests` |
|
|
10
|
+
| `func testFoo()` | `@Test func foo()` |
|
|
11
|
+
| `XCTAssertEqual(a, b)` | `#expect(a == b)` |
|
|
12
|
+
| `XCTAssertTrue(x)` | `#expect(x)` |
|
|
13
|
+
| `XCTAssertFalse(x)` | `#expect(!x)` |
|
|
14
|
+
| `XCTAssertNil(x)` | `#expect(x == nil)` |
|
|
15
|
+
| `XCTAssertNotNil(x)` | `#expect(x != nil)` or `try #require(x)` |
|
|
16
|
+
| `XCTAssertThrowsError` | `#expect(throws:)` |
|
|
17
|
+
| `XCTFail("message")` | `Issue.record("message")` |
|
|
18
|
+
| `XCTSkip("reason")` | Test trait `.disabled("reason")` |
|
|
19
|
+
| `setUp()` | `init()` |
|
|
20
|
+
| `tearDown()` | `deinit` |
|
|
21
|
+
|
|
22
|
+
## Basic Test Migration
|
|
23
|
+
|
|
24
|
+
### Before (XCTest)
|
|
25
|
+
|
|
26
|
+
```swift
|
|
27
|
+
import XCTest
|
|
28
|
+
|
|
29
|
+
class UserTests: XCTestCase {
|
|
30
|
+
func testUserCreation() {
|
|
31
|
+
let user = User(name: "Alice")
|
|
32
|
+
XCTAssertEqual(user.name, "Alice")
|
|
33
|
+
XCTAssertNotNil(user.id)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### After (Swift Testing)
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
import Testing
|
|
42
|
+
|
|
43
|
+
@Suite struct UserTests {
|
|
44
|
+
@Test func userCreation() throws {
|
|
45
|
+
let user = User(name: "Alice")
|
|
46
|
+
#expect(user.name == "Alice")
|
|
47
|
+
let id = try #require(user.id)
|
|
48
|
+
#expect(!id.isEmpty)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Assertion Migration
|
|
54
|
+
|
|
55
|
+
### Equality
|
|
56
|
+
|
|
57
|
+
```swift
|
|
58
|
+
// XCTest
|
|
59
|
+
XCTAssertEqual(result, expected)
|
|
60
|
+
XCTAssertEqual(result, expected, "Custom message")
|
|
61
|
+
|
|
62
|
+
// Swift Testing
|
|
63
|
+
#expect(result == expected)
|
|
64
|
+
#expect(result == expected, "Custom message")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Boolean
|
|
68
|
+
|
|
69
|
+
```swift
|
|
70
|
+
// XCTest
|
|
71
|
+
XCTAssertTrue(condition)
|
|
72
|
+
XCTAssertFalse(condition)
|
|
73
|
+
|
|
74
|
+
// Swift Testing
|
|
75
|
+
#expect(condition)
|
|
76
|
+
#expect(!condition)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Nil Checks
|
|
80
|
+
|
|
81
|
+
```swift
|
|
82
|
+
// XCTest
|
|
83
|
+
XCTAssertNil(optional)
|
|
84
|
+
XCTAssertNotNil(optional)
|
|
85
|
+
|
|
86
|
+
// Swift Testing
|
|
87
|
+
#expect(optional == nil)
|
|
88
|
+
#expect(optional != nil)
|
|
89
|
+
|
|
90
|
+
// Or use #require for unwrapping
|
|
91
|
+
let value = try #require(optional)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Error Testing
|
|
95
|
+
|
|
96
|
+
```swift
|
|
97
|
+
// XCTest
|
|
98
|
+
XCTAssertThrowsError(try riskyOperation()) { error in
|
|
99
|
+
XCTAssertEqual(error as? MyError, .specific)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
XCTAssertNoThrow(try safeOperation())
|
|
103
|
+
|
|
104
|
+
// Swift Testing
|
|
105
|
+
#expect(throws: MyError.specific) {
|
|
106
|
+
try riskyOperation()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#expect(throws: Never.self) {
|
|
110
|
+
try safeOperation()
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Setup and Teardown
|
|
115
|
+
|
|
116
|
+
### Before (XCTest)
|
|
117
|
+
|
|
118
|
+
```swift
|
|
119
|
+
class DatabaseTests: XCTestCase {
|
|
120
|
+
var database: Database!
|
|
121
|
+
|
|
122
|
+
override func setUp() {
|
|
123
|
+
super.setUp()
|
|
124
|
+
database = Database.inMemory()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override func tearDown() {
|
|
128
|
+
database.close()
|
|
129
|
+
database = nil
|
|
130
|
+
super.tearDown()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func testInsert() {
|
|
134
|
+
database.insert(record)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### After (Swift Testing)
|
|
140
|
+
|
|
141
|
+
```swift
|
|
142
|
+
@Suite struct DatabaseTests {
|
|
143
|
+
let database: Database
|
|
144
|
+
|
|
145
|
+
init() throws {
|
|
146
|
+
database = try Database.inMemory()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test func insert() {
|
|
150
|
+
database.insert(record)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Async Tests
|
|
156
|
+
|
|
157
|
+
### Before (XCTest)
|
|
158
|
+
|
|
159
|
+
```swift
|
|
160
|
+
func testAsyncFetch() async throws {
|
|
161
|
+
let result = try await service.fetch()
|
|
162
|
+
XCTAssertFalse(result.isEmpty)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Or with expectations
|
|
166
|
+
func testAsyncWithExpectation() {
|
|
167
|
+
let expectation = XCTestExpectation(description: "Fetch")
|
|
168
|
+
|
|
169
|
+
service.fetch { result in
|
|
170
|
+
XCTAssertNotNil(result)
|
|
171
|
+
expectation.fulfill()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
wait(for: [expectation], timeout: 5)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### After (Swift Testing)
|
|
179
|
+
|
|
180
|
+
```swift
|
|
181
|
+
@Test func asyncFetch() async throws {
|
|
182
|
+
let result = try await service.fetch()
|
|
183
|
+
#expect(!result.isEmpty)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// For callbacks, use confirmation
|
|
187
|
+
@Test func asyncWithConfirmation() async {
|
|
188
|
+
await confirmation { confirm in
|
|
189
|
+
service.fetch { result in
|
|
190
|
+
#expect(result != nil)
|
|
191
|
+
confirm()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Parameterized Tests
|
|
198
|
+
|
|
199
|
+
### Before (XCTest)
|
|
200
|
+
|
|
201
|
+
```swift
|
|
202
|
+
func testValidEmails() {
|
|
203
|
+
let validEmails = ["a@b.com", "test@example.org"]
|
|
204
|
+
for email in validEmails {
|
|
205
|
+
XCTAssertTrue(EmailValidator.isValid(email), "\(email) should be valid")
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### After (Swift Testing)
|
|
211
|
+
|
|
212
|
+
```swift
|
|
213
|
+
@Test(arguments: ["a@b.com", "test@example.org"])
|
|
214
|
+
func validEmail(email: String) {
|
|
215
|
+
#expect(EmailValidator.isValid(email))
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Skipping Tests
|
|
220
|
+
|
|
221
|
+
### Before (XCTest)
|
|
222
|
+
|
|
223
|
+
```swift
|
|
224
|
+
func testPlatformSpecific() throws {
|
|
225
|
+
#if !os(iOS)
|
|
226
|
+
throw XCTSkip("iOS only")
|
|
227
|
+
#endif
|
|
228
|
+
// Test code
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### After (Swift Testing)
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
@Test(.enabled(if: Platform.isIOS, "iOS only"))
|
|
236
|
+
func platformSpecific() {
|
|
237
|
+
// Test code
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Or
|
|
241
|
+
@Test(.disabled("Not implemented yet"))
|
|
242
|
+
func futureFeature() { }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Test Organization
|
|
246
|
+
|
|
247
|
+
### Before (XCTest)
|
|
248
|
+
|
|
249
|
+
```swift
|
|
250
|
+
class CartTests: XCTestCase {
|
|
251
|
+
// Tests grouped by comments
|
|
252
|
+
// MARK: - Adding Items
|
|
253
|
+
func testAddSingleItem() { }
|
|
254
|
+
func testAddMultipleItems() { }
|
|
255
|
+
|
|
256
|
+
// MARK: - Removing Items
|
|
257
|
+
func testRemoveItem() { }
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### After (Swift Testing)
|
|
262
|
+
|
|
263
|
+
```swift
|
|
264
|
+
@Suite("Cart")
|
|
265
|
+
struct CartTests {
|
|
266
|
+
@Suite("Adding Items")
|
|
267
|
+
struct AddingTests {
|
|
268
|
+
@Test func singleItem() { }
|
|
269
|
+
@Test func multipleItems() { }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Suite("Removing Items")
|
|
273
|
+
struct RemovingTests {
|
|
274
|
+
@Test func removeItem() { }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Migration Strategy
|
|
280
|
+
|
|
281
|
+
1. **Start with leaf tests**: Tests that don't depend on XCTest infrastructure
|
|
282
|
+
2. **Migrate one file at a time**: Keep changes reviewable
|
|
283
|
+
3. **Run both simultaneously**: XCTest and Swift Testing can coexist
|
|
284
|
+
4. **Update CI configuration**: Ensure both are run during migration
|
|
285
|
+
5. **Remove XCTest after full migration**: Clean up imports and dependencies
|
|
286
|
+
|
|
287
|
+
## Coexistence
|
|
288
|
+
|
|
289
|
+
You can have both frameworks in the same project:
|
|
290
|
+
|
|
291
|
+
```swift
|
|
292
|
+
// XCTest (existing)
|
|
293
|
+
import XCTest
|
|
294
|
+
class OldTests: XCTestCase { }
|
|
295
|
+
|
|
296
|
+
// Swift Testing (new)
|
|
297
|
+
import Testing
|
|
298
|
+
@Suite struct NewTests { }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Both will be discovered and run by `swift test`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Parameterized Tests
|
|
2
|
+
|
|
3
|
+
Test multiple inputs with a single test function.
|
|
4
|
+
|
|
5
|
+
## Basic Parameterization
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
@Test(arguments: [1, 2, 3, 4, 5])
|
|
9
|
+
func isPositive(number: Int) {
|
|
10
|
+
#expect(number > 0)
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Multiple Arguments
|
|
15
|
+
|
|
16
|
+
### Using zip (Paired)
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
@Test(arguments: zip(
|
|
20
|
+
["hello", "world", "test"],
|
|
21
|
+
[5, 5, 4]
|
|
22
|
+
))
|
|
23
|
+
func stringLength(string: String, expectedLength: Int) {
|
|
24
|
+
#expect(string.count == expectedLength)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Cartesian Product (All Combinations)
|
|
29
|
+
|
|
30
|
+
```swift
|
|
31
|
+
@Test(arguments: [1, 2], ["a", "b"])
|
|
32
|
+
func combinations(number: Int, letter: String) {
|
|
33
|
+
// Runs 4 times: (1,a), (1,b), (2,a), (2,b)
|
|
34
|
+
#expect(!"\(number)\(letter)".isEmpty)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Custom Test Cases
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
struct ValidationTestCase {
|
|
42
|
+
let input: String
|
|
43
|
+
let isValid: Bool
|
|
44
|
+
let description: String
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
extension ValidationTestCase: CustomTestStringConvertible {
|
|
48
|
+
var testDescription: String { description }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let validationCases = [
|
|
52
|
+
ValidationTestCase(input: "valid@email.com", isValid: true, description: "valid email"),
|
|
53
|
+
ValidationTestCase(input: "invalid", isValid: false, description: "missing @"),
|
|
54
|
+
ValidationTestCase(input: "", isValid: false, description: "empty string"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
@Test(arguments: validationCases)
|
|
58
|
+
func validateEmail(testCase: ValidationTestCase) {
|
|
59
|
+
let result = EmailValidator.validate(testCase.input)
|
|
60
|
+
#expect(result == testCase.isValid)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Enum Cases
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
enum Environment: CaseIterable {
|
|
68
|
+
case development, staging, production
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test(arguments: Environment.allCases)
|
|
72
|
+
func configurationLoads(environment: Environment) {
|
|
73
|
+
let config = Configuration(environment: environment)
|
|
74
|
+
#expect(config.isValid)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Ranges
|
|
79
|
+
|
|
80
|
+
```swift
|
|
81
|
+
@Test(arguments: 1...100)
|
|
82
|
+
func withinRange(value: Int) {
|
|
83
|
+
#expect(value >= 1 && value <= 100)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Collection of Tuples
|
|
88
|
+
|
|
89
|
+
```swift
|
|
90
|
+
@Test(arguments: [
|
|
91
|
+
("2024-01-15", true),
|
|
92
|
+
("invalid", false),
|
|
93
|
+
("2024-13-45", false),
|
|
94
|
+
])
|
|
95
|
+
func dateValidation(dateString: String, shouldBeValid: Bool) {
|
|
96
|
+
let isValid = DateValidator.validate(dateString)
|
|
97
|
+
#expect(isValid == shouldBeValid)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Avoiding Cartesian Explosion
|
|
102
|
+
|
|
103
|
+
Be careful with multiple argument lists:
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
// WARNING: This runs 1000 times (10 x 10 x 10)
|
|
107
|
+
@Test(arguments: 1...10, 1...10, 1...10)
|
|
108
|
+
func tooManyTests(a: Int, b: Int, c: Int) { }
|
|
109
|
+
|
|
110
|
+
// BETTER: Use zip for paired testing
|
|
111
|
+
@Test(arguments: zip(zip(inputs1, inputs2), expectedResults))
|
|
112
|
+
func pairedTest(inputs: ((Int, Int), Int)) { }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Filtering Arguments
|
|
116
|
+
|
|
117
|
+
```swift
|
|
118
|
+
let testCases = (1...100).filter { $0 % 10 == 0 }
|
|
119
|
+
|
|
120
|
+
@Test(arguments: testCases)
|
|
121
|
+
func multiplesOfTen(value: Int) {
|
|
122
|
+
#expect(value % 10 == 0)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Complex Test Data
|
|
127
|
+
|
|
128
|
+
```swift
|
|
129
|
+
struct APITestCase: Sendable {
|
|
130
|
+
let endpoint: String
|
|
131
|
+
let method: HTTPMethod
|
|
132
|
+
let expectedStatus: Int
|
|
133
|
+
let body: Data?
|
|
134
|
+
|
|
135
|
+
static let cases: [APITestCase] = [
|
|
136
|
+
APITestCase(endpoint: "/users", method: .get, expectedStatus: 200, body: nil),
|
|
137
|
+
APITestCase(endpoint: "/users", method: .post, expectedStatus: 201, body: validUserData),
|
|
138
|
+
APITestCase(endpoint: "/users/999", method: .get, expectedStatus: 404, body: nil),
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test(arguments: APITestCase.cases)
|
|
143
|
+
func apiEndpoint(testCase: APITestCase) async throws {
|
|
144
|
+
let response = try await client.request(
|
|
145
|
+
endpoint: testCase.endpoint,
|
|
146
|
+
method: testCase.method,
|
|
147
|
+
body: testCase.body
|
|
148
|
+
)
|
|
149
|
+
#expect(response.statusCode == testCase.expectedStatus)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Best Practices
|
|
154
|
+
|
|
155
|
+
1. **Keep test cases focused**: Each should test one thing
|
|
156
|
+
2. **Use descriptive names**: Implement `CustomTestStringConvertible`
|
|
157
|
+
3. **Avoid Cartesian products**: Use zip for paired data
|
|
158
|
+
4. **Group related cases**: Create structs for complex scenarios
|
|
159
|
+
5. **Make test data Sendable**: Required for parallel execution
|
|
160
|
+
|
|
161
|
+
```swift
|
|
162
|
+
// GOOD: Clear, paired test cases
|
|
163
|
+
@Test(arguments: zip(["a", "ab", "abc"], [1, 2, 3]))
|
|
164
|
+
func stringLength(string: String, expected: Int) {
|
|
165
|
+
#expect(string.count == expected)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// BAD: Cartesian product, unclear intent
|
|
169
|
+
@Test(arguments: ["a", "ab", "abc"], [1, 2, 3])
|
|
170
|
+
func unclearTest(string: String, number: Int) { }
|
|
171
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Snapshot Testing
|
|
2
|
+
|
|
3
|
+
Snapshot testing catches visual regressions by comparing rendered UI against recorded baselines.
|
|
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
|
+
import SwiftUI
|
|
20
|
+
@testable import DesignSystem
|
|
21
|
+
|
|
22
|
+
@Suite("PRCelebrationToast Snapshots")
|
|
23
|
+
struct PRCelebrationToastSnapshotTests {
|
|
24
|
+
|
|
25
|
+
@Test("renders correctly for new PR")
|
|
26
|
+
func newPRLayout() {
|
|
27
|
+
let record = PersonalRecord.fixture(liftType: .snatch, weight: 120.0)
|
|
28
|
+
let toast = PRCelebrationToast(
|
|
29
|
+
newPR: record,
|
|
30
|
+
quote: "New personal best!"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assertSnapshot(
|
|
34
|
+
of: toast,
|
|
35
|
+
as: .image(layout: .device(config: .iPhone15Pro))
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Parameterized Snapshots
|
|
42
|
+
|
|
43
|
+
Test multiple configurations:
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
@Test("renders correctly for different lift types", arguments: LiftType.allCases)
|
|
47
|
+
func differentLiftTypes(liftType: LiftType) {
|
|
48
|
+
let record = PersonalRecord.fixture(liftType: liftType, weight: 100.0)
|
|
49
|
+
let toast = PRCelebrationToast(newPR: record, quote: "Great lift!")
|
|
50
|
+
|
|
51
|
+
assertSnapshot(
|
|
52
|
+
of: toast,
|
|
53
|
+
as: .image(layout: .sizeThatFits),
|
|
54
|
+
named: "\(liftType)"
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Layout Options
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
// Device-specific
|
|
63
|
+
.image(layout: .device(config: .iPhone15Pro))
|
|
64
|
+
.image(layout: .device(config: .iPadPro12_9))
|
|
65
|
+
|
|
66
|
+
// Size that fits content
|
|
67
|
+
.image(layout: .sizeThatFits)
|
|
68
|
+
|
|
69
|
+
// Fixed size
|
|
70
|
+
.image(layout: .fixed(width: 300, height: 200))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Recording Mode
|
|
74
|
+
|
|
75
|
+
First run records baselines. To re-record:
|
|
76
|
+
|
|
77
|
+
```swift
|
|
78
|
+
// Re-record all snapshots in this test
|
|
79
|
+
assertSnapshot(of: view, as: .image, record: true)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or use environment variable:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
SNAPSHOT_TESTING_RECORD=1 swift test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Multiple Device Sizes
|
|
89
|
+
|
|
90
|
+
```swift
|
|
91
|
+
@Test("adapts to different screen sizes")
|
|
92
|
+
func multipleDevices() {
|
|
93
|
+
let view = SettingsScreen()
|
|
94
|
+
|
|
95
|
+
let devices: [(String, ViewImageConfig)] = [
|
|
96
|
+
("iPhoneSE", .iPhoneSe),
|
|
97
|
+
("iPhone15Pro", .iPhone15Pro),
|
|
98
|
+
("iPadPro", .iPadPro12_9),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for (name, config) in devices {
|
|
102
|
+
assertSnapshot(
|
|
103
|
+
of: view,
|
|
104
|
+
as: .image(layout: .device(config: config)),
|
|
105
|
+
named: name
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Dark Mode Testing
|
|
112
|
+
|
|
113
|
+
```swift
|
|
114
|
+
@Test("renders correctly in dark mode")
|
|
115
|
+
func darkModeAppearance() {
|
|
116
|
+
let view = SettingsRow(title: "Notifications", isEnabled: true)
|
|
117
|
+
.preferredColorScheme(.dark)
|
|
118
|
+
|
|
119
|
+
assertSnapshot(
|
|
120
|
+
of: view,
|
|
121
|
+
as: .image(layout: .sizeThatFits),
|
|
122
|
+
named: "dark"
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Accessibility Testing
|
|
128
|
+
|
|
129
|
+
```swift
|
|
130
|
+
@Test("supports Dynamic Type")
|
|
131
|
+
func dynamicTypeSupport() {
|
|
132
|
+
let sizes: [ContentSizeCategory] = [.small, .large, .accessibilityExtraExtraLarge]
|
|
133
|
+
|
|
134
|
+
for size in sizes {
|
|
135
|
+
let view = SettingsRow(title: "Notifications", isEnabled: true)
|
|
136
|
+
.environment(\.sizeCategory, size)
|
|
137
|
+
|
|
138
|
+
assertSnapshot(
|
|
139
|
+
of: view,
|
|
140
|
+
as: .image(layout: .sizeThatFits),
|
|
141
|
+
named: "\(size)"
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Best Practices
|
|
148
|
+
|
|
149
|
+
### Consistency
|
|
150
|
+
|
|
151
|
+
- **Same simulator**: Record all snapshots on the same device/simulator
|
|
152
|
+
- **Match CI**: Use same configuration as CI pipeline
|
|
153
|
+
- **Commit baselines**: Store reference images in version control
|
|
154
|
+
|
|
155
|
+
### Organization
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Tests/
|
|
159
|
+
└── DesignSystemTests/
|
|
160
|
+
├── Snapshots/
|
|
161
|
+
│ ├── PRCelebrationToastSnapshotTests.swift
|
|
162
|
+
│ └── __Snapshots__/ # Generated baseline images
|
|
163
|
+
│ └── PRCelebrationToastSnapshotTests/
|
|
164
|
+
│ ├── newPRLayout.png
|
|
165
|
+
│ └── differentLiftTypes-snatch.png
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Review Process
|
|
169
|
+
|
|
170
|
+
1. Run tests locally before PR
|
|
171
|
+
2. Review snapshot diffs carefully
|
|
172
|
+
3. Re-record intentional changes
|
|
173
|
+
4. Commit new baselines with code changes
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
### Flaky Tests
|
|
178
|
+
|
|
179
|
+
```swift
|
|
180
|
+
// Add precision tolerance for anti-aliasing differences
|
|
181
|
+
assertSnapshot(
|
|
182
|
+
of: view,
|
|
183
|
+
as: .image(precision: 0.99)
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### CI Failures
|
|
188
|
+
|
|
189
|
+
- Ensure CI uses same simulator version
|
|
190
|
+
- Consider using `perceptualPrecision` for minor rendering differences
|
|
191
|
+
- Document expected simulator in README
|
|
192
|
+
|
|
193
|
+
### Large Files
|
|
194
|
+
|
|
195
|
+
```swift
|
|
196
|
+
// Use smaller scale for large views
|
|
197
|
+
assertSnapshot(
|
|
198
|
+
of: view,
|
|
199
|
+
as: .image(layout: .fixed(width: 375, height: 812), scale: 1)
|
|
200
|
+
)
|
|
201
|
+
```
|