swift-code-reviewer-skill 1.1.1 → 1.2.1
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 +44 -162
- package/README.md +91 -21
- package/SKILL.md +107 -725
- package/bin/install.js +87 -22
- package/package.json +16 -2
- package/references/companion-skills.md +70 -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
- package/templates/agents/swift-code-reviewer.md +78 -0
- package/templates/commands/review.md +56 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Test Doubles
|
|
2
|
+
|
|
3
|
+
Test doubles help isolate the system under test (SUT) from side effects. Terminology from [Martin Fowler's "Mocks Aren't Stubs"](https://martinfowler.com/articles/mocksArentStubs.html).
|
|
4
|
+
|
|
5
|
+
## State vs Behavior Verification
|
|
6
|
+
|
|
7
|
+
| Approach | Description | Test Doubles Used |
|
|
8
|
+
|----------|-------------|-------------------|
|
|
9
|
+
| **State Verification** | Assert on final state after action | Stubs, Fakes, Spies |
|
|
10
|
+
| **Behavior Verification** | Verify correct calls to collaborators | Mocks |
|
|
11
|
+
|
|
12
|
+
**Prefer state verification** - simpler, less brittle tests.
|
|
13
|
+
|
|
14
|
+
## Dummy
|
|
15
|
+
|
|
16
|
+
A dummy doesn't do anything - just a placeholder:
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
struct UserServiceDummy: UserServiceProtocol {
|
|
20
|
+
func login(_ user: User, completion: (Result<Void, Error>) -> Void) {
|
|
21
|
+
// Does nothing
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Fake
|
|
27
|
+
|
|
28
|
+
Working implementation with shortcuts (e.g., in-memory database):
|
|
29
|
+
|
|
30
|
+
```swift
|
|
31
|
+
final class FavoritesManagerFake: FavoritesManagerProtocol {
|
|
32
|
+
var favorites: [Movie] = []
|
|
33
|
+
|
|
34
|
+
func add(_ movie: Movie) throws {
|
|
35
|
+
guard !favorites.contains(where: { $0.id == movie.id }) else {
|
|
36
|
+
throw FavoritesError.alreadyExists
|
|
37
|
+
}
|
|
38
|
+
favorites.append(movie)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func remove(_ movie: Movie) throws {
|
|
42
|
+
guard let index = favorites.firstIndex(where: { $0.id == movie.id }) else {
|
|
43
|
+
throw FavoritesError.notFound
|
|
44
|
+
}
|
|
45
|
+
favorites.remove(at: index)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Stub
|
|
51
|
+
|
|
52
|
+
Returns pre-configured values:
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
final class PostsServiceStub: PostsServiceProtocol {
|
|
56
|
+
var fetchAllResultToBeReturned: Result<[Post], Error> = .success([])
|
|
57
|
+
|
|
58
|
+
func fetchAll() async throws -> [Post] {
|
|
59
|
+
try fetchAllResultToBeReturned.get()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Naming: [methodName]ToBeReturned
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Spy
|
|
67
|
+
|
|
68
|
+
Records calls for verification:
|
|
69
|
+
|
|
70
|
+
```swift
|
|
71
|
+
final class SafeStorageSpy: SafeStorageProtocol, @unchecked Sendable {
|
|
72
|
+
private(set) var storeUserDataCalled = false
|
|
73
|
+
private(set) var userPassed: User?
|
|
74
|
+
private(set) var storeUserDataCount = 0
|
|
75
|
+
|
|
76
|
+
func storeUserData(_ user: User) {
|
|
77
|
+
storeUserDataCalled = true
|
|
78
|
+
storeUserDataCount += 1
|
|
79
|
+
userPassed = user
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Naming conventions:
|
|
84
|
+
// - Method called: [name]Called (Bool)
|
|
85
|
+
// - Parameter captured: [name]Passed
|
|
86
|
+
// - Call count: [name]Count (Int)
|
|
87
|
+
// - All should be private(set)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## SpyingStub (Most Common)
|
|
91
|
+
|
|
92
|
+
Combines Stub + Spy - this is what Swift developers usually call "Mock":
|
|
93
|
+
|
|
94
|
+
```swift
|
|
95
|
+
final class PersonalRecordsRepositorySpyingStub: PersonalRecordsRepositoryProtocol, @unchecked Sendable {
|
|
96
|
+
// Spy: Captured calls
|
|
97
|
+
private(set) var savedRecords: [PersonalRecord] = []
|
|
98
|
+
private(set) var deletedIds: [UUID] = []
|
|
99
|
+
private(set) var getAllCalled = false
|
|
100
|
+
|
|
101
|
+
// Stub: Configurable responses
|
|
102
|
+
var recordsToReturn: [PersonalRecord] = []
|
|
103
|
+
var errorToThrow: Error?
|
|
104
|
+
|
|
105
|
+
func getAll() async throws -> [PersonalRecord] {
|
|
106
|
+
getAllCalled = true
|
|
107
|
+
if let error = errorToThrow { throw error }
|
|
108
|
+
return recordsToReturn
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func save(_ record: PersonalRecord) async throws {
|
|
112
|
+
if let error = errorToThrow { throw error }
|
|
113
|
+
savedRecords.append(record)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func delete(id: UUID) async throws {
|
|
117
|
+
if let error = errorToThrow { throw error }
|
|
118
|
+
deletedIds.append(id)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Naming: [ProtocolName]SpyingStub
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## True Mock (Fowler Definition)
|
|
126
|
+
|
|
127
|
+
Pre-programmed with expectations, self-verifies:
|
|
128
|
+
|
|
129
|
+
```swift
|
|
130
|
+
final class UserServiceMock: UserServiceProtocol {
|
|
131
|
+
struct Expectation: Equatable {
|
|
132
|
+
let method: String
|
|
133
|
+
let userId: String?
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private var expectations: [Expectation] = []
|
|
137
|
+
private var actualCalls: [Expectation] = []
|
|
138
|
+
private var returnValues: [String: User] = [:]
|
|
139
|
+
|
|
140
|
+
// Setup (before test)
|
|
141
|
+
func expectGetUser(id: String, returning user: User) {
|
|
142
|
+
expectations.append(Expectation(method: "getUser", userId: id))
|
|
143
|
+
returnValues[id] = user
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Protocol implementation
|
|
147
|
+
func getUser(id: String) async throws -> User {
|
|
148
|
+
let call = Expectation(method: "getUser", userId: id)
|
|
149
|
+
actualCalls.append(call)
|
|
150
|
+
|
|
151
|
+
guard expectations.contains(call) else {
|
|
152
|
+
fatalError("Unexpected call: getUser(id: \(id))")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
guard let user = returnValues[id] else {
|
|
156
|
+
throw UserError.notFound
|
|
157
|
+
}
|
|
158
|
+
return user
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Verification (after test)
|
|
162
|
+
func verify() {
|
|
163
|
+
assert(expectations == actualCalls)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Usage
|
|
168
|
+
@Test("fetches user with expected ID")
|
|
169
|
+
func fetchesExpectedUser() async throws {
|
|
170
|
+
let mock = UserServiceMock()
|
|
171
|
+
mock.expectGetUser(id: "123", returning: User.fixture())
|
|
172
|
+
|
|
173
|
+
await sut.loadProfile(userId: "123")
|
|
174
|
+
|
|
175
|
+
mock.verify() // Self-verifies
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Use true mocks when**:
|
|
180
|
+
- Testing interaction protocols (delegates)
|
|
181
|
+
- Verifying exact call sequences
|
|
182
|
+
- Testing that calls are NOT made
|
|
183
|
+
|
|
184
|
+
## Failings (Unimplemented)
|
|
185
|
+
|
|
186
|
+
Fail if unexpectedly called:
|
|
187
|
+
|
|
188
|
+
```swift
|
|
189
|
+
import XCTestDynamicOverlay
|
|
190
|
+
|
|
191
|
+
struct FailingNetworkService: NetworkServiceProtocol {
|
|
192
|
+
func fetchData(from url: URL) async throws -> Data {
|
|
193
|
+
XCTFail("fetchData(from:) was not expected to be called!")
|
|
194
|
+
fatalError()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
With swift-dependencies:
|
|
200
|
+
|
|
201
|
+
```swift
|
|
202
|
+
extension PersonalRecordsRepository: TestDependencyKey {
|
|
203
|
+
static let testValue = PersonalRecordsRepository(
|
|
204
|
+
getAll: unimplemented("\(Self.self).getAll"),
|
|
205
|
+
save: unimplemented("\(Self.self).save")
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Choosing the Right Double
|
|
211
|
+
|
|
212
|
+
| Need | Use |
|
|
213
|
+
|------|-----|
|
|
214
|
+
| Fill a parameter | Dummy |
|
|
215
|
+
| Working lightweight implementation | Fake |
|
|
216
|
+
| Control return values | Stub |
|
|
217
|
+
| Verify calls were made | Spy |
|
|
218
|
+
| Both control and verify | SpyingStub |
|
|
219
|
+
| Verify exact interactions | Mock |
|
|
220
|
+
| Catch unexpected usage | Failing |
|
|
221
|
+
|
|
222
|
+
## Placement
|
|
223
|
+
|
|
224
|
+
Place test doubles **close to the interface**, not in test targets:
|
|
225
|
+
|
|
226
|
+
```swift
|
|
227
|
+
// In ModuleName-Interface/Sources/...
|
|
228
|
+
|
|
229
|
+
public protocol MyServiceProtocol: Sendable {
|
|
230
|
+
func doSomething() async throws
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#if DEBUG
|
|
234
|
+
public final class MyServiceSpyingStub: MyServiceProtocol {
|
|
235
|
+
// Implementation
|
|
236
|
+
}
|
|
237
|
+
#endif
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Benefits:
|
|
241
|
+
- Available to all test targets
|
|
242
|
+
- Lives with the contract it implements
|
|
243
|
+
- Zero production overhead with `#if DEBUG`
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Test Organization
|
|
2
|
+
|
|
3
|
+
Organizing tests with suites, tags, and traits in Swift Testing.
|
|
4
|
+
|
|
5
|
+
## Test Suites
|
|
6
|
+
|
|
7
|
+
Group related tests:
|
|
8
|
+
|
|
9
|
+
```swift
|
|
10
|
+
@Suite("User Management")
|
|
11
|
+
struct UserTests {
|
|
12
|
+
@Test func createUser() { }
|
|
13
|
+
@Test func deleteUser() { }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Suite("Authentication")
|
|
17
|
+
struct AuthTests {
|
|
18
|
+
@Test func login() { }
|
|
19
|
+
@Test func logout() { }
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Nested Suites
|
|
24
|
+
|
|
25
|
+
```swift
|
|
26
|
+
@Suite("Shopping Cart")
|
|
27
|
+
struct CartTests {
|
|
28
|
+
@Suite("Adding Items")
|
|
29
|
+
struct AddTests {
|
|
30
|
+
@Test func addSingleItem() { }
|
|
31
|
+
@Test func addMultipleItems() { }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Suite("Removing Items")
|
|
35
|
+
struct RemoveTests {
|
|
36
|
+
@Test func removeSingleItem() { }
|
|
37
|
+
@Test func clearCart() { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tags
|
|
43
|
+
|
|
44
|
+
Categorize tests for selective running:
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
extension Tag {
|
|
48
|
+
@Tag static var integration: Self
|
|
49
|
+
@Tag static var slow: Self
|
|
50
|
+
@Tag static var network: Self
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test(.tags(.integration))
|
|
54
|
+
func databaseIntegration() { }
|
|
55
|
+
|
|
56
|
+
@Test(.tags(.slow, .network))
|
|
57
|
+
func networkRequest() { }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Running Tagged Tests
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Run only integration tests
|
|
64
|
+
swift test --filter .tags:integration
|
|
65
|
+
|
|
66
|
+
# Exclude slow tests
|
|
67
|
+
swift test --skip .tags:slow
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Traits
|
|
71
|
+
|
|
72
|
+
### Disabled Tests
|
|
73
|
+
|
|
74
|
+
```swift
|
|
75
|
+
@Test(.disabled("Waiting for API fix"))
|
|
76
|
+
func brokenTest() { }
|
|
77
|
+
|
|
78
|
+
@Test(.disabled(if: isCI, "Flaky on CI"))
|
|
79
|
+
func sometimesFlaky() { }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Time Limits
|
|
83
|
+
|
|
84
|
+
```swift
|
|
85
|
+
@Test(.timeLimit(.minutes(1)))
|
|
86
|
+
func slowTest() async { }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Bug References
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
@Test(.bug("https://github.com/org/repo/issues/123"))
|
|
93
|
+
func testWithKnownBug() { }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Custom Traits
|
|
97
|
+
|
|
98
|
+
```swift
|
|
99
|
+
@Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_SLOW_TESTS"] != nil))
|
|
100
|
+
func conditionalTest() { }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Setup and Teardown
|
|
104
|
+
|
|
105
|
+
### Per-Test Setup
|
|
106
|
+
|
|
107
|
+
```swift
|
|
108
|
+
@Suite struct DatabaseTests {
|
|
109
|
+
var database: Database
|
|
110
|
+
|
|
111
|
+
init() throws {
|
|
112
|
+
// Runs before each test
|
|
113
|
+
database = try Database.inMemory()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test func insertRecord() {
|
|
117
|
+
// database is fresh for each test
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Suite-Level Setup
|
|
123
|
+
|
|
124
|
+
```swift
|
|
125
|
+
@Suite struct ServerTests {
|
|
126
|
+
static var server: TestServer!
|
|
127
|
+
|
|
128
|
+
init() async throws {
|
|
129
|
+
// Per-test setup
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test func request() async { }
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Test Organization Best Practices
|
|
137
|
+
|
|
138
|
+
### File Structure
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Tests/
|
|
142
|
+
├── UnitTests/
|
|
143
|
+
│ ├── Models/
|
|
144
|
+
│ │ ├── UserTests.swift
|
|
145
|
+
│ │ └── ProductTests.swift
|
|
146
|
+
│ ├── Services/
|
|
147
|
+
│ │ ├── AuthServiceTests.swift
|
|
148
|
+
│ │ └── CartServiceTests.swift
|
|
149
|
+
│ └── Utilities/
|
|
150
|
+
│ └── FormatterTests.swift
|
|
151
|
+
├── IntegrationTests/
|
|
152
|
+
│ ├── DatabaseTests.swift
|
|
153
|
+
│ └── APITests.swift
|
|
154
|
+
└── TestHelpers/
|
|
155
|
+
├── Fixtures.swift
|
|
156
|
+
└── Mocks.swift
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Naming Files
|
|
160
|
+
|
|
161
|
+
- Name test files after the type they test: `UserTests.swift` for `User`
|
|
162
|
+
- Use `Tests` suffix for test files
|
|
163
|
+
|
|
164
|
+
### Organizing Within Files
|
|
165
|
+
|
|
166
|
+
```swift
|
|
167
|
+
@Suite("User")
|
|
168
|
+
struct UserTests {
|
|
169
|
+
// MARK: - Initialization
|
|
170
|
+
|
|
171
|
+
@Test func initWithValidData() { }
|
|
172
|
+
@Test func initWithInvalidData() { }
|
|
173
|
+
|
|
174
|
+
// MARK: - Properties
|
|
175
|
+
|
|
176
|
+
@Test func fullName() { }
|
|
177
|
+
@Test func age() { }
|
|
178
|
+
|
|
179
|
+
// MARK: - Methods
|
|
180
|
+
|
|
181
|
+
@Test func update() { }
|
|
182
|
+
@Test func delete() { }
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Test Discovery
|
|
187
|
+
|
|
188
|
+
Swift Testing automatically discovers:
|
|
189
|
+
- Functions marked with `@Test`
|
|
190
|
+
- Types marked with `@Suite`
|
|
191
|
+
- Nested suites and tests
|
|
192
|
+
|
|
193
|
+
No need to:
|
|
194
|
+
- Inherit from XCTestCase
|
|
195
|
+
- Prefix with "test"
|
|
196
|
+
- Register tests manually
|
|
197
|
+
|
|
198
|
+
## Parallel Execution
|
|
199
|
+
|
|
200
|
+
Tests run in parallel by default:
|
|
201
|
+
|
|
202
|
+
```swift
|
|
203
|
+
@Suite(.serialized) // Run tests in this suite serially
|
|
204
|
+
struct SerialTests {
|
|
205
|
+
@Test func first() { }
|
|
206
|
+
@Test func second() { }
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## FIRST Principles
|
|
211
|
+
|
|
212
|
+
Structure tests to be:
|
|
213
|
+
|
|
214
|
+
- **Fast**: Run quickly
|
|
215
|
+
- **Isolated**: No dependencies between tests
|
|
216
|
+
- **Repeatable**: Same result every time
|
|
217
|
+
- **Self-validating**: Clear pass/fail
|
|
218
|
+
- **Timely**: Written with or before code
|
|
219
|
+
|
|
220
|
+
```swift
|
|
221
|
+
@Test func fastAndIsolated() {
|
|
222
|
+
// Uses in-memory database, not real one
|
|
223
|
+
let db = Database.inMemory()
|
|
224
|
+
|
|
225
|
+
// Self-contained data
|
|
226
|
+
let user = User.fixture()
|
|
227
|
+
|
|
228
|
+
// Clear assertion
|
|
229
|
+
#expect(db.save(user))
|
|
230
|
+
}
|
|
231
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# swiftui-expert-skill — Attribution Notice
|
|
2
|
+
|
|
3
|
+
Bundled into `swift-code-reviewer-skill` on 2026-04-21 from
|
|
4
|
+
`~/.agents/skills/swiftui-expert-skill`.
|
|
5
|
+
|
|
6
|
+
Primary author (best-effort attribution): **Thomas Ricouard ([@Dimillian](https://github.com/Dimillian))**
|
|
7
|
+
Also credited: [@AvdLee](https://github.com/AvdLee), [@bocato](https://github.com/bocato)
|
|
8
|
+
|
|
9
|
+
These three authors' public Swift/SwiftUI content — including IceCubesApp, SwiftLee,
|
|
10
|
+
and their various open-source contributions — informed the skills bundled here.
|
|
11
|
+
|
|
12
|
+
License: the upstream folder did not contain a LICENSE file at the time of vendoring.
|
|
13
|
+
Content is reproduced here in good faith for reference alongside this MIT-licensed
|
|
14
|
+
project. If you are an upstream author and want the attribution corrected, the license
|
|
15
|
+
clarified, or the content removed, please open an issue at:
|
|
16
|
+
https://github.com/Viniciuscarvalho/swift-code-reviewer-skill/issues
|
|
17
|
+
|
|
18
|
+
Changes from upstream: none (verbatim copy; `.DS_Store` files excluded).
|