agentic-team-templates 0.13.2 → 0.15.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/README.md +6 -1
- package/package.json +1 -1
- package/src/index.js +91 -13
- package/src/index.test.js +95 -1
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# SwiftUI
|
|
2
|
+
|
|
3
|
+
Declarative UI with composable views, state management, and the observation framework.
|
|
4
|
+
|
|
5
|
+
## View Fundamentals
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
struct UserProfileView: View {
|
|
9
|
+
let user: User
|
|
10
|
+
|
|
11
|
+
var body: some View {
|
|
12
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
13
|
+
AsyncImage(url: user.avatarURL) { image in
|
|
14
|
+
image.resizable().scaledToFill()
|
|
15
|
+
} placeholder: {
|
|
16
|
+
ProgressView()
|
|
17
|
+
}
|
|
18
|
+
.frame(width: 80, height: 80)
|
|
19
|
+
.clipShape(Circle())
|
|
20
|
+
|
|
21
|
+
Text(user.name)
|
|
22
|
+
.font(.title2)
|
|
23
|
+
.fontWeight(.bold)
|
|
24
|
+
|
|
25
|
+
Text(user.bio)
|
|
26
|
+
.font(.body)
|
|
27
|
+
.foregroundStyle(.secondary)
|
|
28
|
+
}
|
|
29
|
+
.padding()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## State Management
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
// @State — view-local mutable state (value types)
|
|
38
|
+
struct CounterView: View {
|
|
39
|
+
@State private var count = 0
|
|
40
|
+
|
|
41
|
+
var body: some View {
|
|
42
|
+
Button("Count: \(count)") { count += 1 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// @Binding — two-way connection to parent's state
|
|
47
|
+
struct ToggleRow: View {
|
|
48
|
+
let title: String
|
|
49
|
+
@Binding var isOn: Bool
|
|
50
|
+
|
|
51
|
+
var body: some View {
|
|
52
|
+
Toggle(title, isOn: $isOn)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// @StateObject — owned reference type (create once)
|
|
57
|
+
struct UserListView: View {
|
|
58
|
+
@StateObject private var viewModel = UserListViewModel()
|
|
59
|
+
|
|
60
|
+
var body: some View {
|
|
61
|
+
List(viewModel.users) { user in
|
|
62
|
+
UserRow(user: user)
|
|
63
|
+
}
|
|
64
|
+
.task { await viewModel.loadUsers() }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// @ObservedObject — non-owned reference type (passed in)
|
|
69
|
+
struct UserDetailView: View {
|
|
70
|
+
@ObservedObject var viewModel: UserDetailViewModel
|
|
71
|
+
// ...
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// @EnvironmentObject — shared dependency via environment
|
|
75
|
+
struct SettingsView: View {
|
|
76
|
+
@EnvironmentObject var theme: ThemeManager
|
|
77
|
+
// ...
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Observation Framework (iOS 17+)
|
|
82
|
+
|
|
83
|
+
```swift
|
|
84
|
+
// @Observable replaces ObservableObject
|
|
85
|
+
@Observable
|
|
86
|
+
class UserViewModel {
|
|
87
|
+
var users: [User] = []
|
|
88
|
+
var isLoading = false
|
|
89
|
+
var error: AppError?
|
|
90
|
+
|
|
91
|
+
func loadUsers() async {
|
|
92
|
+
isLoading = true
|
|
93
|
+
defer { isLoading = false }
|
|
94
|
+
do {
|
|
95
|
+
users = try await userService.fetchAll()
|
|
96
|
+
} catch {
|
|
97
|
+
self.error = AppError(error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// No @Published needed. SwiftUI tracks property access automatically
|
|
103
|
+
struct UserListView: View {
|
|
104
|
+
var viewModel = UserViewModel()
|
|
105
|
+
|
|
106
|
+
var body: some View {
|
|
107
|
+
List(viewModel.users) { user in
|
|
108
|
+
Text(user.name)
|
|
109
|
+
}
|
|
110
|
+
.overlay { if viewModel.isLoading { ProgressView() } }
|
|
111
|
+
.task { await viewModel.loadUsers() }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Navigation
|
|
117
|
+
|
|
118
|
+
```swift
|
|
119
|
+
// NavigationStack with type-safe routing
|
|
120
|
+
enum Route: Hashable {
|
|
121
|
+
case userDetail(User)
|
|
122
|
+
case settings
|
|
123
|
+
case editProfile
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
struct ContentView: View {
|
|
127
|
+
@State private var path = NavigationPath()
|
|
128
|
+
|
|
129
|
+
var body: some View {
|
|
130
|
+
NavigationStack(path: $path) {
|
|
131
|
+
HomeView()
|
|
132
|
+
.navigationDestination(for: Route.self) { route in
|
|
133
|
+
switch route {
|
|
134
|
+
case .userDetail(let user):
|
|
135
|
+
UserDetailView(user: user)
|
|
136
|
+
case .settings:
|
|
137
|
+
SettingsView()
|
|
138
|
+
case .editProfile:
|
|
139
|
+
EditProfileView()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Composition Patterns
|
|
148
|
+
|
|
149
|
+
```swift
|
|
150
|
+
// Small, composable views
|
|
151
|
+
struct AvatarView: View {
|
|
152
|
+
let url: URL?
|
|
153
|
+
var size: CGFloat = 40
|
|
154
|
+
|
|
155
|
+
var body: some View {
|
|
156
|
+
AsyncImage(url: url) { image in
|
|
157
|
+
image.resizable().scaledToFill()
|
|
158
|
+
} placeholder: {
|
|
159
|
+
Image(systemName: "person.circle.fill")
|
|
160
|
+
.foregroundStyle(.secondary)
|
|
161
|
+
}
|
|
162
|
+
.frame(width: size, height: size)
|
|
163
|
+
.clipShape(Circle())
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ViewModifier for reusable styling
|
|
168
|
+
struct CardModifier: ViewModifier {
|
|
169
|
+
func body(content: Content) -> some View {
|
|
170
|
+
content
|
|
171
|
+
.padding()
|
|
172
|
+
.background(.background)
|
|
173
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
174
|
+
.shadow(radius: 2)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
extension View {
|
|
179
|
+
func cardStyle() -> some View {
|
|
180
|
+
modifier(CardModifier())
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Usage: Text("Hello").cardStyle()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Lists and Performance
|
|
188
|
+
|
|
189
|
+
```swift
|
|
190
|
+
// Lazy loading
|
|
191
|
+
struct UserListView: View {
|
|
192
|
+
let users: [User]
|
|
193
|
+
|
|
194
|
+
var body: some View {
|
|
195
|
+
List {
|
|
196
|
+
ForEach(users) { user in
|
|
197
|
+
UserRow(user: user)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
.listStyle(.plain)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// LazyVStack for custom layouts
|
|
205
|
+
ScrollView {
|
|
206
|
+
LazyVStack(spacing: 8) {
|
|
207
|
+
ForEach(items) { item in
|
|
208
|
+
ItemView(item: item) // Only created when visible
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Previews
|
|
215
|
+
|
|
216
|
+
```swift
|
|
217
|
+
#Preview("User Profile") {
|
|
218
|
+
UserProfileView(user: .preview)
|
|
219
|
+
.padding()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#Preview("Loading State") {
|
|
223
|
+
UserProfileView(user: .preview)
|
|
224
|
+
.redacted(reason: .placeholder)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Preview data
|
|
228
|
+
extension User {
|
|
229
|
+
static var preview: User {
|
|
230
|
+
User(id: UUID(), name: "Jane Doe", email: "jane@example.com")
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Anti-Patterns
|
|
236
|
+
|
|
237
|
+
```swift
|
|
238
|
+
// Never: massive body properties
|
|
239
|
+
var body: some View {
|
|
240
|
+
// 200 lines of nested views
|
|
241
|
+
}
|
|
242
|
+
// Use: extract subviews as separate structs or computed properties
|
|
243
|
+
|
|
244
|
+
// Never: business logic in views
|
|
245
|
+
var body: some View {
|
|
246
|
+
Button("Submit") {
|
|
247
|
+
let valid = email.contains("@") && password.count >= 8
|
|
248
|
+
if valid { /* ... */ }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Use: view model with dedicated validation
|
|
252
|
+
|
|
253
|
+
// Never: @ObservedObject for view-created objects
|
|
254
|
+
@ObservedObject var vm = MyViewModel() // Recreated on every view update!
|
|
255
|
+
// Use: @StateObject for view-owned objects
|
|
256
|
+
|
|
257
|
+
// Never: ignoring .task lifecycle
|
|
258
|
+
.onAppear { Task { await load() } } // No automatic cancellation
|
|
259
|
+
// Use: .task { await load() } — cancelled when view disappears
|
|
260
|
+
```
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Swift Testing
|
|
2
|
+
|
|
3
|
+
XCTest, Swift Testing framework, and UI testing. Test behavior with expressive assertions.
|
|
4
|
+
|
|
5
|
+
## Framework Stack
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| Swift Testing | Modern test framework (Swift 5.10+) |
|
|
10
|
+
| XCTest | Traditional test framework |
|
|
11
|
+
| ViewInspector | SwiftUI view testing |
|
|
12
|
+
| XCUITest | UI automation testing |
|
|
13
|
+
| swift-snapshot-testing | Snapshot/screenshot testing |
|
|
14
|
+
| OHHTTPStubs / URLProtocol | Network mocking |
|
|
15
|
+
|
|
16
|
+
## Swift Testing Framework
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
import Testing
|
|
20
|
+
|
|
21
|
+
@Suite("UserService")
|
|
22
|
+
struct UserServiceTests {
|
|
23
|
+
let sut: UserService
|
|
24
|
+
let mockRepo: MockUserRepository
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
mockRepo = MockUserRepository()
|
|
28
|
+
sut = UserService(repository: mockRepo)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test("creates user with valid input")
|
|
32
|
+
func createUserValid() async throws {
|
|
33
|
+
let request = CreateUserRequest(name: "Alice", email: "alice@test.com")
|
|
34
|
+
|
|
35
|
+
let user = try await sut.create(request)
|
|
36
|
+
|
|
37
|
+
#expect(user.name == "Alice")
|
|
38
|
+
#expect(user.email == "alice@test.com")
|
|
39
|
+
#expect(user.id != UUID())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test("rejects duplicate email")
|
|
43
|
+
func rejectsDuplicateEmail() async {
|
|
44
|
+
mockRepo.existingEmails = ["alice@test.com"]
|
|
45
|
+
let request = CreateUserRequest(name: "Alice", email: "alice@test.com")
|
|
46
|
+
|
|
47
|
+
await #expect(throws: ValidationError.duplicateEmail) {
|
|
48
|
+
try await sut.create(request)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test("validates email format", arguments: [
|
|
53
|
+
"invalid",
|
|
54
|
+
"@missing.local",
|
|
55
|
+
"no-at-sign",
|
|
56
|
+
"",
|
|
57
|
+
])
|
|
58
|
+
func invalidEmails(email: String) async {
|
|
59
|
+
let request = CreateUserRequest(name: "Test", email: email)
|
|
60
|
+
|
|
61
|
+
await #expect(throws: ValidationError.invalidEmail) {
|
|
62
|
+
try await sut.create(request)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## XCTest (Traditional)
|
|
69
|
+
|
|
70
|
+
```swift
|
|
71
|
+
final class OrderServiceTests: XCTestCase {
|
|
72
|
+
private var sut: OrderService!
|
|
73
|
+
private var mockInventory: MockInventoryClient!
|
|
74
|
+
|
|
75
|
+
override func setUp() {
|
|
76
|
+
super.setUp()
|
|
77
|
+
mockInventory = MockInventoryClient()
|
|
78
|
+
sut = OrderService(inventory: mockInventory)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override func tearDown() {
|
|
82
|
+
sut = nil
|
|
83
|
+
mockInventory = nil
|
|
84
|
+
super.tearDown()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func test_createOrder_withValidItems_succeeds() async throws {
|
|
88
|
+
// Arrange
|
|
89
|
+
mockInventory.availableStock = ["SKU-001": 10]
|
|
90
|
+
let request = CreateOrderRequest(items: [.init(sku: "SKU-001", quantity: 2)])
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
let order = try await sut.create(request)
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
XCTAssertEqual(order.items.count, 1)
|
|
97
|
+
XCTAssertEqual(order.status, .pending)
|
|
98
|
+
XCTAssertEqual(mockInventory.reserveCalls.count, 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func test_createOrder_withInsufficientStock_throws() async {
|
|
102
|
+
mockInventory.availableStock = ["SKU-001": 0]
|
|
103
|
+
let request = CreateOrderRequest(items: [.init(sku: "SKU-001", quantity: 5)])
|
|
104
|
+
|
|
105
|
+
do {
|
|
106
|
+
_ = try await sut.create(request)
|
|
107
|
+
XCTFail("Expected OrderError.insufficientStock")
|
|
108
|
+
} catch let error as OrderError {
|
|
109
|
+
XCTAssertEqual(error, .insufficientStock("SKU-001"))
|
|
110
|
+
} catch {
|
|
111
|
+
XCTFail("Unexpected error: \(error)")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Async Testing
|
|
118
|
+
|
|
119
|
+
```swift
|
|
120
|
+
// Swift Testing — async is native
|
|
121
|
+
@Test func fetchUsers() async throws {
|
|
122
|
+
let users = try await sut.fetchAll()
|
|
123
|
+
#expect(users.count == 3)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// XCTest — async/await support
|
|
127
|
+
func test_fetchUsers() async throws {
|
|
128
|
+
let users = try await sut.fetchAll()
|
|
129
|
+
XCTAssertEqual(users.count, 3)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Testing with timeouts (XCTest)
|
|
133
|
+
func test_longRunningOperation() async throws {
|
|
134
|
+
let expectation = expectation(description: "completes")
|
|
135
|
+
|
|
136
|
+
Task {
|
|
137
|
+
_ = try await sut.processLargeDataset()
|
|
138
|
+
expectation.fulfill()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await fulfillment(of: [expectation], timeout: 10)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Mocking with Protocols
|
|
146
|
+
|
|
147
|
+
```swift
|
|
148
|
+
// Protocol-based dependency
|
|
149
|
+
protocol UserRepository {
|
|
150
|
+
func findById(_ id: UUID) async throws -> User?
|
|
151
|
+
func save(_ user: User) async throws -> User
|
|
152
|
+
func delete(_ id: UUID) async throws
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Mock implementation
|
|
156
|
+
final class MockUserRepository: UserRepository {
|
|
157
|
+
var users: [UUID: User] = [:]
|
|
158
|
+
var saveCalls: [User] = []
|
|
159
|
+
var deleteCalls: [UUID] = []
|
|
160
|
+
var findByIdError: Error?
|
|
161
|
+
|
|
162
|
+
func findById(_ id: UUID) async throws -> User? {
|
|
163
|
+
if let error = findByIdError { throw error }
|
|
164
|
+
return users[id]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func save(_ user: User) async throws -> User {
|
|
168
|
+
saveCalls.append(user)
|
|
169
|
+
users[user.id] = user
|
|
170
|
+
return user
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func delete(_ id: UUID) async throws {
|
|
174
|
+
deleteCalls.append(id)
|
|
175
|
+
users.removeValue(forKey: id)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Network Mocking
|
|
181
|
+
|
|
182
|
+
```swift
|
|
183
|
+
// URLProtocol-based mock
|
|
184
|
+
class MockURLProtocol: URLProtocol {
|
|
185
|
+
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
|
|
186
|
+
|
|
187
|
+
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
188
|
+
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
189
|
+
|
|
190
|
+
override func startLoading() {
|
|
191
|
+
guard let handler = Self.requestHandler else {
|
|
192
|
+
fatalError("requestHandler not set")
|
|
193
|
+
}
|
|
194
|
+
do {
|
|
195
|
+
let (response, data) = try handler(request)
|
|
196
|
+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
197
|
+
client?.urlProtocol(self, didLoad: data)
|
|
198
|
+
client?.urlProtocolDidFinishLoading(self)
|
|
199
|
+
} catch {
|
|
200
|
+
client?.urlProtocol(self, didFailWithError: error)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
override func stopLoading() {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Usage in tests
|
|
208
|
+
func test_fetchUser_decodesResponse() async throws {
|
|
209
|
+
let userData = try JSONEncoder().encode(User.preview)
|
|
210
|
+
MockURLProtocol.requestHandler = { request in
|
|
211
|
+
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
|
212
|
+
return (response, userData)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let config = URLSessionConfiguration.ephemeral
|
|
216
|
+
config.protocolClasses = [MockURLProtocol.self]
|
|
217
|
+
let session = URLSession(configuration: config)
|
|
218
|
+
let client = APIClient(session: session)
|
|
219
|
+
|
|
220
|
+
let user = try await client.fetchUser(id: UUID())
|
|
221
|
+
XCTAssertEqual(user.name, "Jane Doe")
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## UI Testing
|
|
226
|
+
|
|
227
|
+
```swift
|
|
228
|
+
final class LoginUITests: XCTestCase {
|
|
229
|
+
let app = XCUIApplication()
|
|
230
|
+
|
|
231
|
+
override func setUp() {
|
|
232
|
+
super.setUp()
|
|
233
|
+
continueAfterFailure = false
|
|
234
|
+
app.launchArguments = ["--uitesting"]
|
|
235
|
+
app.launch()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func test_loginFlow_withValidCredentials() {
|
|
239
|
+
let emailField = app.textFields["email-field"]
|
|
240
|
+
emailField.tap()
|
|
241
|
+
emailField.typeText("user@test.com")
|
|
242
|
+
|
|
243
|
+
let passwordField = app.secureTextFields["password-field"]
|
|
244
|
+
passwordField.tap()
|
|
245
|
+
passwordField.typeText("password123")
|
|
246
|
+
|
|
247
|
+
app.buttons["login-button"].tap()
|
|
248
|
+
|
|
249
|
+
XCTAssertTrue(app.staticTexts["welcome-label"].waitForExistence(timeout: 5))
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Test Naming
|
|
255
|
+
|
|
256
|
+
```swift
|
|
257
|
+
// XCTest: test_[method]_[condition]_[expected]
|
|
258
|
+
func test_validate_emptyEmail_throwsValidationError()
|
|
259
|
+
func test_create_validRequest_returnsNewUser()
|
|
260
|
+
func test_delete_nonexistentUser_throwsNotFound()
|
|
261
|
+
|
|
262
|
+
// Swift Testing: descriptive strings
|
|
263
|
+
@Test("validates email format rejects empty string")
|
|
264
|
+
@Test("creates user with all required fields")
|
|
265
|
+
@Test("deletes existing user successfully")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Anti-Patterns
|
|
269
|
+
|
|
270
|
+
```swift
|
|
271
|
+
// Never: testing implementation details
|
|
272
|
+
XCTAssertEqual(viewModel.fetchCallCount, 1) // Fragile
|
|
273
|
+
// Test the observable outcome instead
|
|
274
|
+
|
|
275
|
+
// Never: shared mutable state between tests
|
|
276
|
+
static var sharedDatabase = Database() // Tests affect each other
|
|
277
|
+
// Use: setUp/tearDown for fresh state
|
|
278
|
+
|
|
279
|
+
// Never: testing with real network calls
|
|
280
|
+
let user = try await realAPIClient.fetchUser(id: uuid) // Flaky
|
|
281
|
+
// Use: protocol mocking or URLProtocol
|
|
282
|
+
|
|
283
|
+
// Never: sleeping in tests
|
|
284
|
+
try await Task.sleep(for: .seconds(2)) // Slow and flaky
|
|
285
|
+
// Use: expectations with timeouts, or test schedulers
|
|
286
|
+
```
|