@aicgen/aicgen 1.0.0-beta.2 → 1.0.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/.agent/rules/api-design.md +649 -0
- package/.agent/rules/architecture.md +2507 -0
- package/.agent/rules/best-practices.md +622 -0
- package/.agent/rules/code-style.md +308 -0
- package/.agent/rules/design-patterns.md +577 -0
- package/.agent/rules/devops.md +230 -0
- package/.agent/rules/error-handling.md +417 -0
- package/.agent/rules/instructions.md +28 -0
- package/.agent/rules/language.md +786 -0
- package/.agent/rules/performance.md +710 -0
- package/.agent/rules/security.md +587 -0
- package/.agent/rules/testing.md +572 -0
- package/.agent/workflows/add-documentation.md +10 -0
- package/.agent/workflows/generate-integration-tests.md +10 -0
- package/.agent/workflows/generate-unit-tests.md +11 -0
- package/.agent/workflows/performance-audit.md +11 -0
- package/.agent/workflows/refactor-extract-module.md +12 -0
- package/.agent/workflows/security-audit.md +12 -0
- package/.gemini/instructions.md +4843 -0
- package/AGENTS.md +9 -11
- package/bun.lock +755 -4
- package/claude.md +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/guideline-mappings.yml +128 -0
- package/data/language/dart/async.md +289 -0
- package/data/language/dart/basics.md +280 -0
- package/data/language/dart/error-handling.md +355 -0
- package/data/language/dart/index.md +10 -0
- package/data/language/dart/testing.md +352 -0
- package/data/language/swift/basics.md +477 -0
- package/data/language/swift/concurrency.md +654 -0
- package/data/language/swift/error-handling.md +679 -0
- package/data/language/swift/swiftui-mvvm.md +795 -0
- package/data/language/swift/testing.md +708 -0
- package/data/version.json +10 -8
- package/dist/index.js +50295 -29101
- package/jest.config.js +46 -0
- package/package.json +13 -2
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# Swift Testing
|
|
2
|
+
|
|
3
|
+
## Swift Testing Framework (Swift 6+)
|
|
4
|
+
|
|
5
|
+
The new Swift Testing framework (introduced at WWDC24) provides a modern, expressive API for testing:
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
import Testing
|
|
9
|
+
|
|
10
|
+
// ✅ Simple test with @Test macro
|
|
11
|
+
@Test func additionWorks() {
|
|
12
|
+
#expect(2 + 2 == 4)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ✅ Test with description
|
|
16
|
+
@Test("Addition produces correct results")
|
|
17
|
+
func testAddition() {
|
|
18
|
+
let result = Calculator().add(2, 3)
|
|
19
|
+
#expect(result == 5)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ✅ Async test
|
|
23
|
+
@Test func fetchUserReturnsValidData() async throws {
|
|
24
|
+
let user = try await fetchUser(id: "123")
|
|
25
|
+
#expect(user.id == "123")
|
|
26
|
+
#expect(!user.name.isEmpty)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ✅ Test with throws
|
|
30
|
+
@Test func divisionByZeroThrows() throws {
|
|
31
|
+
#expect(throws: DivisionError.self) {
|
|
32
|
+
try Calculator().divide(10, by: 0)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Test Suites
|
|
38
|
+
|
|
39
|
+
Organize tests into logical groups:
|
|
40
|
+
|
|
41
|
+
```swift
|
|
42
|
+
import Testing
|
|
43
|
+
|
|
44
|
+
// ✅ Test suite with @Suite macro
|
|
45
|
+
@Suite("Calculator Tests")
|
|
46
|
+
struct CalculatorTests {
|
|
47
|
+
let calculator = Calculator()
|
|
48
|
+
|
|
49
|
+
@Test("Addition")
|
|
50
|
+
func testAddition() {
|
|
51
|
+
#expect(calculator.add(2, 3) == 5)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test("Subtraction")
|
|
55
|
+
func testSubtraction() {
|
|
56
|
+
#expect(calculator.subtract(5, 3) == 2)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ✅ Nested suites for organization
|
|
61
|
+
@Suite("User Management")
|
|
62
|
+
struct UserTests {
|
|
63
|
+
@Suite("Authentication")
|
|
64
|
+
struct AuthTests {
|
|
65
|
+
@Test func loginSucceeds() async throws {
|
|
66
|
+
let result = try await login(email: "test@example.com", password: "password")
|
|
67
|
+
#expect(result.isSuccess)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test func loginFailsWithInvalidCredentials() async throws {
|
|
71
|
+
await #expect(throws: AuthError.self) {
|
|
72
|
+
try await login(email: "test@example.com", password: "wrong")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Suite("Profile Management")
|
|
78
|
+
struct ProfileTests {
|
|
79
|
+
@Test func updateProfileSucceeds() async throws {
|
|
80
|
+
let user = try await updateProfile(name: "New Name")
|
|
81
|
+
#expect(user.name == "New Name")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Parameterized Tests
|
|
88
|
+
|
|
89
|
+
Test multiple scenarios efficiently:
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
import Testing
|
|
93
|
+
|
|
94
|
+
// ✅ Test with multiple arguments
|
|
95
|
+
@Test(arguments: [
|
|
96
|
+
(2, 3, 5),
|
|
97
|
+
(0, 0, 0),
|
|
98
|
+
(-1, 1, 0),
|
|
99
|
+
(100, 200, 300)
|
|
100
|
+
])
|
|
101
|
+
func additionWorks(a: Int, b: Int, expected: Int) {
|
|
102
|
+
let calculator = Calculator()
|
|
103
|
+
#expect(calculator.add(a, b) == expected)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ✅ Test with array of values
|
|
107
|
+
@Test(arguments: ["alice@example.com", "bob@test.com", "charlie@mail.com"])
|
|
108
|
+
func emailValidationWorks(email: String) {
|
|
109
|
+
#expect(EmailValidator.isValid(email))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ✅ Test combinations
|
|
113
|
+
@Test(arguments: [true, false], [1, 2, 3])
|
|
114
|
+
func featureWorksInAllCombinations(flag: Bool, value: Int) {
|
|
115
|
+
let result = processFeature(enabled: flag, value: value)
|
|
116
|
+
#expect(result != nil)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ✅ Test with custom types
|
|
120
|
+
struct TestCase {
|
|
121
|
+
let input: String
|
|
122
|
+
let expected: Bool
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Test(arguments: [
|
|
126
|
+
TestCase(input: "hello", expected: true),
|
|
127
|
+
TestCase(input: "", expected: false),
|
|
128
|
+
TestCase(input: "a", expected: true)
|
|
129
|
+
])
|
|
130
|
+
func validation(testCase: TestCase) {
|
|
131
|
+
#expect(validate(testCase.input) == testCase.expected)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Expectations and Assertions
|
|
136
|
+
|
|
137
|
+
### Basic Expectations
|
|
138
|
+
|
|
139
|
+
```swift
|
|
140
|
+
import Testing
|
|
141
|
+
|
|
142
|
+
// ✅ Boolean expectations
|
|
143
|
+
#expect(value == 5)
|
|
144
|
+
#expect(value > 0)
|
|
145
|
+
#expect(name.isEmpty)
|
|
146
|
+
#expect(!isLoading)
|
|
147
|
+
|
|
148
|
+
// ✅ Optional expectations
|
|
149
|
+
#expect(optionalValue != nil)
|
|
150
|
+
#expect(users.first?.name == "Alice")
|
|
151
|
+
|
|
152
|
+
// ✅ Collection expectations
|
|
153
|
+
#expect(array.count == 3)
|
|
154
|
+
#expect(array.contains(item))
|
|
155
|
+
#expect(dictionary["key"] == "value")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Error Expectations
|
|
159
|
+
|
|
160
|
+
```swift
|
|
161
|
+
// ✅ Expect specific error type
|
|
162
|
+
#expect(throws: NetworkError.self) {
|
|
163
|
+
try performNetworkRequest()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ✅ Expect any error
|
|
167
|
+
#expect(throws: Error.self) {
|
|
168
|
+
try riskyOperation()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ✅ Expect no error (default behavior)
|
|
172
|
+
#expect {
|
|
173
|
+
try safeOperation()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ✅ Async error expectations
|
|
177
|
+
await #expect(throws: APIError.unauthorized) {
|
|
178
|
+
try await fetchProtectedResource()
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Custom Error Messages
|
|
183
|
+
|
|
184
|
+
```swift
|
|
185
|
+
// ✅ Add context to failures
|
|
186
|
+
#expect(user.age >= 18, "User must be at least 18 years old")
|
|
187
|
+
#expect(items.count > 0, "Shopping cart cannot be empty")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Test Lifecycle
|
|
191
|
+
|
|
192
|
+
### Setup and Teardown
|
|
193
|
+
|
|
194
|
+
```swift
|
|
195
|
+
import Testing
|
|
196
|
+
|
|
197
|
+
@Suite("Database Tests")
|
|
198
|
+
struct DatabaseTests {
|
|
199
|
+
let database: Database
|
|
200
|
+
|
|
201
|
+
// ✅ Initialize resources before each test
|
|
202
|
+
init() async throws {
|
|
203
|
+
database = try await Database.connect()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ✅ Clean up after tests
|
|
207
|
+
deinit {
|
|
208
|
+
database.disconnect()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Test func insertRecordWorks() async throws {
|
|
212
|
+
try await database.insert(record)
|
|
213
|
+
let fetched = try await database.fetch(id: record.id)
|
|
214
|
+
#expect(fetched == record)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## XCTest (Traditional Framework)
|
|
220
|
+
|
|
221
|
+
For backwards compatibility and existing codebases:
|
|
222
|
+
|
|
223
|
+
```swift
|
|
224
|
+
import XCTest
|
|
225
|
+
|
|
226
|
+
// ✅ XCTest class
|
|
227
|
+
class UserServiceTests: XCTestCase {
|
|
228
|
+
var service: UserService!
|
|
229
|
+
|
|
230
|
+
// Setup before each test
|
|
231
|
+
override func setUp() {
|
|
232
|
+
super.setUp()
|
|
233
|
+
service = UserService()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Teardown after each test
|
|
237
|
+
override func tearDown() {
|
|
238
|
+
service = nil
|
|
239
|
+
super.tearDown()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ✅ Test method (must start with "test")
|
|
243
|
+
func testFetchUserSucceeds() async throws {
|
|
244
|
+
let user = try await service.fetchUser(id: "123")
|
|
245
|
+
XCTAssertEqual(user.id, "123")
|
|
246
|
+
XCTAssertFalse(user.name.isEmpty)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ✅ Test expected error
|
|
250
|
+
func testFetchUserThrowsForInvalidID() async {
|
|
251
|
+
await XCTAssertThrowsError(try await service.fetchUser(id: "invalid")) { error in
|
|
252
|
+
XCTAssertTrue(error is ServiceError)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ✅ Async test
|
|
257
|
+
func testAsyncOperation() async throws {
|
|
258
|
+
let result = try await service.performAsyncOperation()
|
|
259
|
+
XCTAssertTrue(result.success)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Common XCTest Assertions
|
|
265
|
+
|
|
266
|
+
```swift
|
|
267
|
+
// Equality
|
|
268
|
+
XCTAssertEqual(actual, expected)
|
|
269
|
+
XCTAssertNotEqual(actual, unexpected)
|
|
270
|
+
|
|
271
|
+
// Boolean
|
|
272
|
+
XCTAssertTrue(condition)
|
|
273
|
+
XCTAssertFalse(condition)
|
|
274
|
+
|
|
275
|
+
// Nil checks
|
|
276
|
+
XCTAssertNil(optionalValue)
|
|
277
|
+
XCTAssertNotNil(optionalValue)
|
|
278
|
+
|
|
279
|
+
// Errors
|
|
280
|
+
XCTAssertThrowsError(try dangerousOperation())
|
|
281
|
+
XCTAssertNoThrow(try safeOperation())
|
|
282
|
+
|
|
283
|
+
// Numeric comparisons
|
|
284
|
+
XCTAssertGreaterThan(value, 0)
|
|
285
|
+
XCTAssertLessThan(value, 100)
|
|
286
|
+
|
|
287
|
+
// Custom messages
|
|
288
|
+
XCTAssertEqual(user.age, 18, "User age should be 18")
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Testing Async Code
|
|
292
|
+
|
|
293
|
+
### Swift Testing Framework
|
|
294
|
+
|
|
295
|
+
```swift
|
|
296
|
+
import Testing
|
|
297
|
+
|
|
298
|
+
// ✅ Simple async test
|
|
299
|
+
@Test func fetchDataCompletes() async throws {
|
|
300
|
+
let data = try await fetchData()
|
|
301
|
+
#expect(!data.isEmpty)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ✅ Test with timeout (future feature)
|
|
305
|
+
@Test(.timeLimit(.seconds(5)))
|
|
306
|
+
func slowOperationCompletes() async throws {
|
|
307
|
+
try await performSlowOperation()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ✅ Test concurrent operations
|
|
311
|
+
@Test func concurrentFetchesSucceed() async throws {
|
|
312
|
+
async let user1 = fetchUser(id: "1")
|
|
313
|
+
async let user2 = fetchUser(id: "2")
|
|
314
|
+
async let user3 = fetchUser(id: "3")
|
|
315
|
+
|
|
316
|
+
let users = try await [user1, user2, user3]
|
|
317
|
+
#expect(users.count == 3)
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### XCTest with Async
|
|
322
|
+
|
|
323
|
+
```swift
|
|
324
|
+
import XCTest
|
|
325
|
+
|
|
326
|
+
class AsyncTests: XCTestCase {
|
|
327
|
+
// ✅ Async test method
|
|
328
|
+
func testAsyncOperation() async throws {
|
|
329
|
+
let result = try await fetchData()
|
|
330
|
+
XCTAssertFalse(result.isEmpty)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ✅ Test with expectation (older pattern)
|
|
334
|
+
func testCallbackCompletion() {
|
|
335
|
+
let expectation = expectation(description: "Callback called")
|
|
336
|
+
|
|
337
|
+
fetchDataWithCallback { result in
|
|
338
|
+
XCTAssertNotNil(result)
|
|
339
|
+
expectation.fulfill()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
waitForExpectations(timeout: 5)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Mocking and Test Doubles
|
|
348
|
+
|
|
349
|
+
### Protocol-Based Mocking
|
|
350
|
+
|
|
351
|
+
```swift
|
|
352
|
+
// ✅ Define protocol for dependency
|
|
353
|
+
protocol UserRepository {
|
|
354
|
+
func fetchUser(id: String) async throws -> User
|
|
355
|
+
func saveUser(_ user: User) async throws
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Real implementation
|
|
359
|
+
class ProductionUserRepository: UserRepository {
|
|
360
|
+
func fetchUser(id: String) async throws -> User {
|
|
361
|
+
// Real API call
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func saveUser(_ user: User) async throws {
|
|
365
|
+
// Real save
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ✅ Mock for testing
|
|
370
|
+
class MockUserRepository: UserRepository {
|
|
371
|
+
var fetchUserCalled = false
|
|
372
|
+
var userToReturn: User?
|
|
373
|
+
var errorToThrow: Error?
|
|
374
|
+
|
|
375
|
+
func fetchUser(id: String) async throws -> User {
|
|
376
|
+
fetchUserCalled = true
|
|
377
|
+
|
|
378
|
+
if let error = errorToThrow {
|
|
379
|
+
throw error
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
guard let user = userToReturn else {
|
|
383
|
+
throw MockError.noUserSet
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return user
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func saveUser(_ user: User) async throws {
|
|
390
|
+
// Store for verification
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Test usage
|
|
395
|
+
@Test func serviceUsesMockRepository() async throws {
|
|
396
|
+
let mockRepo = MockUserRepository()
|
|
397
|
+
mockRepo.userToReturn = User(id: "123", name: "Alice")
|
|
398
|
+
|
|
399
|
+
let service = UserService(repository: mockRepo)
|
|
400
|
+
let user = try await service.getUser(id: "123")
|
|
401
|
+
|
|
402
|
+
#expect(mockRepo.fetchUserCalled)
|
|
403
|
+
#expect(user.name == "Alice")
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Spy Pattern
|
|
408
|
+
|
|
409
|
+
```swift
|
|
410
|
+
// ✅ Spy records method calls
|
|
411
|
+
class SpyUserRepository: UserRepository {
|
|
412
|
+
var fetchUserCallCount = 0
|
|
413
|
+
var fetchedUserIDs: [String] = []
|
|
414
|
+
var savedUsers: [User] = []
|
|
415
|
+
|
|
416
|
+
func fetchUser(id: String) async throws -> User {
|
|
417
|
+
fetchUserCallCount += 1
|
|
418
|
+
fetchedUserIDs.append(id)
|
|
419
|
+
return User(id: id, name: "Test User")
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
func saveUser(_ user: User) async throws {
|
|
423
|
+
savedUsers.append(user)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@Test func serviceCallsRepositoryCorrectly() async throws {
|
|
428
|
+
let spy = SpyUserRepository()
|
|
429
|
+
let service = UserService(repository: spy)
|
|
430
|
+
|
|
431
|
+
try await service.getUser(id: "123")
|
|
432
|
+
try await service.getUser(id: "456")
|
|
433
|
+
|
|
434
|
+
#expect(spy.fetchUserCallCount == 2)
|
|
435
|
+
#expect(spy.fetchedUserIDs == ["123", "456"])
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Testing Best Practices
|
|
440
|
+
|
|
441
|
+
### Write Focused Tests
|
|
442
|
+
|
|
443
|
+
```swift
|
|
444
|
+
// ❌ Testing too much in one test
|
|
445
|
+
@Test func userManagement() async throws {
|
|
446
|
+
let user = try await createUser(name: "Alice")
|
|
447
|
+
let updated = try await updateUser(user, name: "Bob")
|
|
448
|
+
try await deleteUser(updated.id)
|
|
449
|
+
let fetched = try? await fetchUser(id: updated.id)
|
|
450
|
+
#expect(fetched == nil)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ✅ One test per behavior
|
|
454
|
+
@Test func createUserSucceeds() async throws {
|
|
455
|
+
let user = try await createUser(name: "Alice")
|
|
456
|
+
#expect(user.name == "Alice")
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@Test func updateUserChangesName() async throws {
|
|
460
|
+
let user = try await createUser(name: "Alice")
|
|
461
|
+
let updated = try await updateUser(user, name: "Bob")
|
|
462
|
+
#expect(updated.name == "Bob")
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@Test func deleteUserRemovesFromDatabase() async throws {
|
|
466
|
+
let user = try await createUser(name: "Alice")
|
|
467
|
+
try await deleteUser(user.id)
|
|
468
|
+
await #expect(throws: NotFoundError.self) {
|
|
469
|
+
try await fetchUser(id: user.id)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Test Edge Cases
|
|
475
|
+
|
|
476
|
+
```swift
|
|
477
|
+
@Suite("String Validation Tests")
|
|
478
|
+
struct ValidationTests {
|
|
479
|
+
@Test func validEmailPasses() {
|
|
480
|
+
#expect(validate("test@example.com"))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@Test func emptyStringFails() {
|
|
484
|
+
#expect(!validate(""))
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@Test func stringWithoutAtSymbolFails() {
|
|
488
|
+
#expect(!validate("notanemail"))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@Test func veryLongEmailFails() {
|
|
492
|
+
let longEmail = String(repeating: "a", count: 300) + "@example.com"
|
|
493
|
+
#expect(!validate(longEmail))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@Test func emailWithSpecialCharactersWorks() {
|
|
497
|
+
#expect(validate("user+tag@example.co.uk"))
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Use Descriptive Names
|
|
503
|
+
|
|
504
|
+
```swift
|
|
505
|
+
// ❌ Unclear test names
|
|
506
|
+
@Test func test1() { }
|
|
507
|
+
@Test func testUser() { }
|
|
508
|
+
|
|
509
|
+
// ✅ Descriptive names that explain what is tested
|
|
510
|
+
@Test("User registration succeeds with valid email")
|
|
511
|
+
func userRegistrationWithValidEmail() { }
|
|
512
|
+
|
|
513
|
+
@Test("Login fails when password is incorrect")
|
|
514
|
+
func loginWithIncorrectPassword() { }
|
|
515
|
+
|
|
516
|
+
@Test("Shopping cart total includes tax")
|
|
517
|
+
func cartTotalCalculation() { }
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Avoid Test Interdependence
|
|
521
|
+
|
|
522
|
+
```swift
|
|
523
|
+
// ❌ Tests depend on execution order
|
|
524
|
+
class BadTests: XCTestCase {
|
|
525
|
+
static var userId: String?
|
|
526
|
+
|
|
527
|
+
func test1_createUser() {
|
|
528
|
+
BadTests.userId = createUser().id
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
func test2_updateUser() {
|
|
532
|
+
updateUser(id: BadTests.userId!) // Fails if test1 doesn't run first
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ✅ Each test is independent
|
|
537
|
+
@Suite("User Tests")
|
|
538
|
+
struct GoodTests {
|
|
539
|
+
@Test func userCreation() async throws {
|
|
540
|
+
let user = try await createUser(name: "Alice")
|
|
541
|
+
#expect(user.name == "Alice")
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@Test func userUpdate() async throws {
|
|
545
|
+
// Create user within this test
|
|
546
|
+
let user = try await createUser(name: "Alice")
|
|
547
|
+
let updated = try await updateUser(user, name: "Bob")
|
|
548
|
+
#expect(updated.name == "Bob")
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Testing View Models
|
|
554
|
+
|
|
555
|
+
```swift
|
|
556
|
+
import Testing
|
|
557
|
+
|
|
558
|
+
@Suite("User View Model Tests")
|
|
559
|
+
@MainActor
|
|
560
|
+
struct UserViewModelTests {
|
|
561
|
+
@Test func loadingUsersUpdatesState() async {
|
|
562
|
+
let mockRepo = MockUserRepository()
|
|
563
|
+
mockRepo.usersToReturn = [
|
|
564
|
+
User(id: "1", name: "Alice"),
|
|
565
|
+
User(id: "2", name: "Bob")
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
let viewModel = UserViewModel(repository: mockRepo)
|
|
569
|
+
await viewModel.loadUsers()
|
|
570
|
+
|
|
571
|
+
#expect(viewModel.users.count == 2)
|
|
572
|
+
#expect(!viewModel.isLoading)
|
|
573
|
+
#expect(viewModel.errorMessage == nil)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
@Test func loadingUsersHandlesErrors() async {
|
|
577
|
+
let mockRepo = MockUserRepository()
|
|
578
|
+
mockRepo.errorToThrow = NetworkError.connectionFailed
|
|
579
|
+
|
|
580
|
+
let viewModel = UserViewModel(repository: mockRepo)
|
|
581
|
+
await viewModel.loadUsers()
|
|
582
|
+
|
|
583
|
+
#expect(viewModel.users.isEmpty)
|
|
584
|
+
#expect(!viewModel.isLoading)
|
|
585
|
+
#expect(viewModel.errorMessage != nil)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@Test func deletingUserRemovesFromList() async {
|
|
589
|
+
let mockRepo = MockUserRepository()
|
|
590
|
+
let user1 = User(id: "1", name: "Alice")
|
|
591
|
+
let user2 = User(id: "2", name: "Bob")
|
|
592
|
+
mockRepo.usersToReturn = [user1, user2]
|
|
593
|
+
|
|
594
|
+
let viewModel = UserViewModel(repository: mockRepo)
|
|
595
|
+
await viewModel.loadUsers()
|
|
596
|
+
await viewModel.deleteUser(user1)
|
|
597
|
+
|
|
598
|
+
#expect(viewModel.users.count == 1)
|
|
599
|
+
#expect(viewModel.users.first?.id == "2")
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## Performance Testing
|
|
605
|
+
|
|
606
|
+
### Swift Testing
|
|
607
|
+
|
|
608
|
+
```swift
|
|
609
|
+
import Testing
|
|
610
|
+
|
|
611
|
+
// ✅ Measure performance
|
|
612
|
+
@Test func sortingLargeArray() {
|
|
613
|
+
let array = (0..<10000).shuffled()
|
|
614
|
+
measure {
|
|
615
|
+
_ = array.sorted()
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### XCTest Performance
|
|
621
|
+
|
|
622
|
+
```swift
|
|
623
|
+
import XCTest
|
|
624
|
+
|
|
625
|
+
class PerformanceTests: XCTestCase {
|
|
626
|
+
func testSortingPerformance() {
|
|
627
|
+
let array = (0..<10000).shuffled()
|
|
628
|
+
|
|
629
|
+
measure {
|
|
630
|
+
_ = array.sorted()
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
func testDatabaseQueryPerformance() async {
|
|
635
|
+
await measureAsync {
|
|
636
|
+
try? await database.fetchAllRecords()
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
extension XCTestCase {
|
|
642
|
+
func measureAsync(_ block: @escaping () async -> Void) async {
|
|
643
|
+
measure {
|
|
644
|
+
let expectation = expectation(description: "Async operation")
|
|
645
|
+
Task {
|
|
646
|
+
await block()
|
|
647
|
+
expectation.fulfill()
|
|
648
|
+
}
|
|
649
|
+
wait(for: [expectation], timeout: 10)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Test Organization
|
|
656
|
+
|
|
657
|
+
### File Structure
|
|
658
|
+
|
|
659
|
+
```
|
|
660
|
+
Tests/
|
|
661
|
+
├── UnitTests/
|
|
662
|
+
│ ├── Models/
|
|
663
|
+
│ │ ├── UserTests.swift
|
|
664
|
+
│ │ └── ProductTests.swift
|
|
665
|
+
│ ├── Services/
|
|
666
|
+
│ │ ├── UserServiceTests.swift
|
|
667
|
+
│ │ └── AuthServiceTests.swift
|
|
668
|
+
│ └── ViewModels/
|
|
669
|
+
│ ├── UserViewModelTests.swift
|
|
670
|
+
│ └── ProductViewModelTests.swift
|
|
671
|
+
├── IntegrationTests/
|
|
672
|
+
│ ├── APITests.swift
|
|
673
|
+
│ └── DatabaseTests.swift
|
|
674
|
+
└── TestHelpers/
|
|
675
|
+
├── Mocks.swift
|
|
676
|
+
└── Fixtures.swift
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Shared Test Utilities
|
|
680
|
+
|
|
681
|
+
```swift
|
|
682
|
+
// ✅ Create reusable test helpers
|
|
683
|
+
enum TestFixtures {
|
|
684
|
+
static func makeUser(id: String = "123", name: String = "Test User") -> User {
|
|
685
|
+
User(id: id, name: name, email: "\(name)@test.com")
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
static func makeUsers(count: Int) -> [User] {
|
|
689
|
+
(0..<count).map { i in
|
|
690
|
+
makeUser(id: "\(i)", name: "User \(i)")
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Usage in tests
|
|
696
|
+
@Test func processMultipleUsers() {
|
|
697
|
+
let users = TestFixtures.makeUsers(count: 5)
|
|
698
|
+
#expect(users.count == 5)
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
**Sources:**
|
|
705
|
+
- [Swift Testing (WWDC24)](https://developer.apple.com/videos/play/wwdc2024/10179/)
|
|
706
|
+
- [Apple XCTest Documentation](https://developer.apple.com/documentation/xctest)
|
|
707
|
+
- [Testing Swift](https://www.swiftbysundell.com/basics/testing/)
|
|
708
|
+
- [Swift.org: Testing](https://www.swift.org/documentation/testing/)
|