@aicgen/aicgen 1.0.0-beta.1 → 1.0.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/{.claude/guidelines → .agent/rules}/api-design.md +5 -1
- package/{.claude/guidelines → .agent/rules}/architecture.md +5 -1
- package/{.claude/guidelines → .agent/rules}/best-practices.md +5 -1
- package/{.claude/guidelines → .agent/rules}/code-style.md +5 -1
- package/{.claude/guidelines → .agent/rules}/design-patterns.md +5 -1
- package/{.claude/guidelines → .agent/rules}/devops.md +5 -1
- package/{.claude/guidelines → .agent/rules}/error-handling.md +5 -1
- package/.agent/rules/instructions.md +28 -0
- package/{.claude/guidelines → .agent/rules}/language.md +5 -1
- package/{.claude/guidelines → .agent/rules}/performance.md +5 -1
- package/{.claude/guidelines → .agent/rules}/security.md +5 -1
- package/{.claude/guidelines → .agent/rules}/testing.md +5 -1
- 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/.vs/ProjectSettings.json +2 -2
- package/.vs/VSWorkspaceState.json +15 -15
- package/.vs/aicgen.slnx/v18/DocumentLayout.json +53 -53
- package/AGENTS.md +9 -11
- package/assets/icon.svg +33 -33
- package/bun.lock +734 -26
- package/{CLAUDE.md → claude.md} +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/architecture/microservices/api-gateway.md +56 -56
- package/data/devops/observability.md +73 -73
- 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 +50153 -28959
- package/jest.config.js +46 -0
- package/package.json +14 -3
- package/.claude/agents/architecture-reviewer.md +0 -88
- package/.claude/agents/guideline-checker.md +0 -73
- package/.claude/agents/security-auditor.md +0 -108
- package/.claude/settings.json +0 -98
- package/.claude/settings.local.json +0 -8
- package/.eslintrc.json +0 -28
- package/.github/workflows/release.yml +0 -180
- package/.github/workflows/test.yml +0 -81
- package/CONTRIBUTING.md +0 -821
- package/dist/commands/init.d.ts +0 -8
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -46
- package/dist/commands/init.js.map +0 -1
- package/dist/config/profiles.d.ts +0 -4
- package/dist/config/profiles.d.ts.map +0 -1
- package/dist/config/profiles.js +0 -30
- package/dist/config/profiles.js.map +0 -1
- package/dist/config/settings.d.ts +0 -7
- package/dist/config/settings.d.ts.map +0 -1
- package/dist/config/settings.js +0 -7
- package/dist/config/settings.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/models/guideline.d.ts +0 -15
- package/dist/models/guideline.d.ts.map +0 -1
- package/dist/models/guideline.js +0 -2
- package/dist/models/guideline.js.map +0 -1
- package/dist/models/preference.d.ts +0 -9
- package/dist/models/preference.d.ts.map +0 -1
- package/dist/models/preference.js +0 -2
- package/dist/models/preference.js.map +0 -1
- package/dist/models/profile.d.ts +0 -9
- package/dist/models/profile.d.ts.map +0 -1
- package/dist/models/profile.js +0 -2
- package/dist/models/profile.js.map +0 -1
- package/dist/models/project.d.ts +0 -13
- package/dist/models/project.d.ts.map +0 -1
- package/dist/models/project.js +0 -2
- package/dist/models/project.js.map +0 -1
- package/dist/services/ai/anthropic.d.ts +0 -7
- package/dist/services/ai/anthropic.d.ts.map +0 -1
- package/dist/services/ai/anthropic.js +0 -39
- package/dist/services/ai/anthropic.js.map +0 -1
- package/dist/services/generator.d.ts +0 -2
- package/dist/services/generator.d.ts.map +0 -1
- package/dist/services/generator.js +0 -4
- package/dist/services/generator.js.map +0 -1
- package/dist/services/learner.d.ts +0 -2
- package/dist/services/learner.d.ts.map +0 -1
- package/dist/services/learner.js +0 -4
- package/dist/services/learner.js.map +0 -1
- package/dist/services/scanner.d.ts +0 -3
- package/dist/services/scanner.d.ts.map +0 -1
- package/dist/services/scanner.js +0 -54
- package/dist/services/scanner.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -15
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/file.d.ts +0 -7
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -32
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -17
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/path.d.ts +0 -6
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -14
- package/dist/utils/path.js.map +0 -1
- package/docs/planning/memory-lane.md +0 -83
- package/packaging/linux/aicgen.spec +0 -23
- package/packaging/linux/control +0 -9
- package/packaging/macos/scripts/postinstall +0 -12
- package/packaging/windows/setup.nsi +0 -92
- package/scripts/add-categories.ts +0 -87
- package/scripts/build-binary.ts +0 -46
- package/scripts/embed-data.ts +0 -105
- package/scripts/generate-version.ts +0 -150
- package/scripts/test-decompress.ts +0 -27
- package/scripts/test-extract.ts +0 -31
- package/src/__tests__/services/assistant-file-writer.test.ts +0 -400
- package/src/__tests__/services/guideline-loader.test.ts +0 -281
- package/src/__tests__/services/tarball-extraction.test.ts +0 -125
- package/src/commands/add-guideline.ts +0 -296
- package/src/commands/clear.ts +0 -61
- package/src/commands/guideline-selector.ts +0 -123
- package/src/commands/init.ts +0 -645
- package/src/commands/quick-add.ts +0 -586
- package/src/commands/remove-guideline.ts +0 -152
- package/src/commands/stats.ts +0 -49
- package/src/commands/update.ts +0 -240
- package/src/config.ts +0 -82
- package/src/embedded-data.ts +0 -1492
- package/src/index.ts +0 -67
- package/src/models/profile.ts +0 -24
- package/src/models/project.ts +0 -43
- package/src/services/assistant-file-writer.ts +0 -612
- package/src/services/config-generator.ts +0 -150
- package/src/services/config-manager.ts +0 -70
- package/src/services/data-source.ts +0 -248
- package/src/services/first-run-init.ts +0 -148
- package/src/services/guideline-loader.ts +0 -311
- package/src/services/hook-generator.ts +0 -178
- package/src/services/subagent-generator.ts +0 -310
- package/src/utils/banner.ts +0 -66
- package/src/utils/errors.ts +0 -27
- package/src/utils/file.ts +0 -67
- package/src/utils/formatting.ts +0 -172
- package/src/utils/logger.ts +0 -89
- package/src/utils/path.ts +0 -17
- package/src/utils/wizard-state.ts +0 -132
- package/tsconfig.json +0 -25
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Error Handling in Dart
|
|
2
|
+
|
|
3
|
+
## Custom Exceptions
|
|
4
|
+
|
|
5
|
+
```dart
|
|
6
|
+
// ✅ Create structured exception hierarchy
|
|
7
|
+
class AppException implements Exception {
|
|
8
|
+
final String message;
|
|
9
|
+
final int? code;
|
|
10
|
+
final dynamic details;
|
|
11
|
+
|
|
12
|
+
AppException(this.message, {this.code, this.details});
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
String toString() => 'AppException: $message (code: $code)';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class NotFoundException extends AppException {
|
|
19
|
+
NotFoundException(String resource, String id)
|
|
20
|
+
: super('$resource with id $id not found', code: 404);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ValidationException extends AppException {
|
|
24
|
+
ValidationException(String message, {Map<String, String>? errors})
|
|
25
|
+
: super(message, code: 400, details: errors);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Usage
|
|
29
|
+
throw NotFoundException('User', userId);
|
|
30
|
+
throw ValidationException('Invalid input', errors: {'email': 'Invalid format'});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Try-Catch-Finally
|
|
34
|
+
|
|
35
|
+
```dart
|
|
36
|
+
// ✅ Catch specific exceptions first
|
|
37
|
+
Future<User> fetchUser(String id) async {
|
|
38
|
+
try {
|
|
39
|
+
final response = await http.get('/api/users/$id');
|
|
40
|
+
return User.fromJson(response.data);
|
|
41
|
+
} on NetworkException catch (e) {
|
|
42
|
+
print('Network error: ${e.message}');
|
|
43
|
+
throw AppException('Network request failed', details: e);
|
|
44
|
+
} on FormatException catch (e) {
|
|
45
|
+
print('Invalid JSON: $e');
|
|
46
|
+
throw AppException('Invalid response format');
|
|
47
|
+
} catch (e, stackTrace) {
|
|
48
|
+
print('Unexpected error: $e');
|
|
49
|
+
print('Stack trace: $stackTrace');
|
|
50
|
+
rethrow;
|
|
51
|
+
} finally {
|
|
52
|
+
print('Request completed');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ✅ Use finally for cleanup
|
|
57
|
+
Future<void> processFile(String path) async {
|
|
58
|
+
File? file;
|
|
59
|
+
try {
|
|
60
|
+
file = File(path);
|
|
61
|
+
final content = await file.readAsString();
|
|
62
|
+
await processContent(content);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
print('Error processing file: $e');
|
|
65
|
+
rethrow;
|
|
66
|
+
} finally {
|
|
67
|
+
// Always runs, even if exception thrown
|
|
68
|
+
await file?.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Result Type Pattern
|
|
74
|
+
|
|
75
|
+
```dart
|
|
76
|
+
// ✅ Explicit success/failure without exceptions
|
|
77
|
+
class Result<T> {
|
|
78
|
+
final T? data;
|
|
79
|
+
final AppException? error;
|
|
80
|
+
|
|
81
|
+
const Result.success(T data) : data = data, error = null;
|
|
82
|
+
const Result.failure(AppException error) : data = null, error = error;
|
|
83
|
+
|
|
84
|
+
bool get isSuccess => error == null;
|
|
85
|
+
bool get isFailure => error != null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Usage
|
|
89
|
+
Future<Result<User>> fetchUserSafe(String id) async {
|
|
90
|
+
try {
|
|
91
|
+
final user = await fetchUser(id);
|
|
92
|
+
return Result.success(user);
|
|
93
|
+
} on AppException catch (e) {
|
|
94
|
+
return Result.failure(e);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return Result.failure(AppException('Unknown error: $e'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Consumer handles explicitly
|
|
101
|
+
final result = await fetchUserSafe('123');
|
|
102
|
+
if (result.isSuccess) {
|
|
103
|
+
print('User: ${result.data!.name}');
|
|
104
|
+
} else {
|
|
105
|
+
print('Error: ${result.error!.message}');
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Either Type Pattern
|
|
110
|
+
|
|
111
|
+
```dart
|
|
112
|
+
// ✅ Functional error handling
|
|
113
|
+
sealed class Either<L, R> {
|
|
114
|
+
const Either();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class Left<L, R> extends Either<L, R> {
|
|
118
|
+
final L value;
|
|
119
|
+
const Left(this.value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class Right<L, R> extends Either<L, R> {
|
|
123
|
+
final R value;
|
|
124
|
+
const Right(this.value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Usage
|
|
128
|
+
Future<Either<AppException, User>> fetchUser(String id) async {
|
|
129
|
+
try {
|
|
130
|
+
final user = await _fetchUser(id);
|
|
131
|
+
return Right(user);
|
|
132
|
+
} on AppException catch (e) {
|
|
133
|
+
return Left(e);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Pattern matching (Dart 3.0+)
|
|
138
|
+
final result = await fetchUser('123');
|
|
139
|
+
switch (result) {
|
|
140
|
+
case Left(value: final error):
|
|
141
|
+
print('Error: ${error.message}');
|
|
142
|
+
case Right(value: final user):
|
|
143
|
+
print('User: ${user.name}');
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Validation
|
|
148
|
+
|
|
149
|
+
```dart
|
|
150
|
+
// ✅ Validate early, throw specific errors
|
|
151
|
+
class UserValidator {
|
|
152
|
+
static void validate(String email, String password) {
|
|
153
|
+
if (email.isEmpty) {
|
|
154
|
+
throw ValidationException('Email is required');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!email.contains('@')) {
|
|
158
|
+
throw ValidationException('Invalid email format');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (password.length < 8) {
|
|
162
|
+
throw ValidationException('Password must be at least 8 characters');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Usage
|
|
168
|
+
try {
|
|
169
|
+
UserValidator.validate(email, password);
|
|
170
|
+
await createUser(email, password);
|
|
171
|
+
} on ValidationException catch (e) {
|
|
172
|
+
showError(e.message);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Assert for Development
|
|
177
|
+
|
|
178
|
+
```dart
|
|
179
|
+
// ✅ Use assert for development-time checks
|
|
180
|
+
void transfer(Account from, Account to, double amount) {
|
|
181
|
+
assert(amount > 0, 'Amount must be positive');
|
|
182
|
+
assert(from.balance >= amount, 'Insufficient funds');
|
|
183
|
+
|
|
184
|
+
from.withdraw(amount);
|
|
185
|
+
to.deposit(amount);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Assertions only run in debug mode
|
|
189
|
+
// In production, they're removed
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Error Boundaries (Flutter)
|
|
193
|
+
|
|
194
|
+
```dart
|
|
195
|
+
// ✅ Catch errors at widget level
|
|
196
|
+
class ErrorBoundary extends StatefulWidget {
|
|
197
|
+
final Widget child;
|
|
198
|
+
|
|
199
|
+
const ErrorBoundary({required this.child});
|
|
200
|
+
|
|
201
|
+
@override
|
|
202
|
+
State<ErrorBoundary> createState() => _ErrorBoundaryState();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class _ErrorBoundaryState extends State<ErrorBoundary> {
|
|
206
|
+
Object? error;
|
|
207
|
+
|
|
208
|
+
@override
|
|
209
|
+
Widget build(BuildContext context) {
|
|
210
|
+
if (error != null) {
|
|
211
|
+
return ErrorWidget(error: error!);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return ErrorWrapper(
|
|
215
|
+
onError: (error, stackTrace) {
|
|
216
|
+
setState(() => this.error = error);
|
|
217
|
+
},
|
|
218
|
+
child: widget.child,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Global Error Handling
|
|
225
|
+
|
|
226
|
+
```dart
|
|
227
|
+
// ✅ Catch uncaught errors
|
|
228
|
+
void main() {
|
|
229
|
+
// Synchronous errors
|
|
230
|
+
FlutterError.onError = (details) {
|
|
231
|
+
print('Flutter error: ${details.exception}');
|
|
232
|
+
print('Stack trace: ${details.stack}');
|
|
233
|
+
reportError(details.exception, details.stack);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Asynchronous errors
|
|
237
|
+
PlatformDispatcher.instance.onError = (error, stack) {
|
|
238
|
+
print('Async error: $error');
|
|
239
|
+
reportError(error, stack);
|
|
240
|
+
return true;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
runApp(MyApp());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void reportError(Object error, StackTrace? stackTrace) {
|
|
247
|
+
// Send to error tracking service
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Retry Pattern
|
|
252
|
+
|
|
253
|
+
```dart
|
|
254
|
+
// ✅ Retry with exponential backoff
|
|
255
|
+
Future<T> retryWithBackoff<T>(
|
|
256
|
+
Future<T> Function() operation, {
|
|
257
|
+
int maxAttempts = 3,
|
|
258
|
+
Duration initialDelay = const Duration(seconds: 1),
|
|
259
|
+
}) async {
|
|
260
|
+
int attempt = 0;
|
|
261
|
+
while (true) {
|
|
262
|
+
try {
|
|
263
|
+
return await operation();
|
|
264
|
+
} catch (e) {
|
|
265
|
+
attempt++;
|
|
266
|
+
if (attempt >= maxAttempts) rethrow;
|
|
267
|
+
|
|
268
|
+
final delay = initialDelay * (1 << attempt); // Exponential backoff
|
|
269
|
+
print('Retry attempt $attempt after $delay');
|
|
270
|
+
await Future.delayed(delay);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Usage
|
|
276
|
+
final user = await retryWithBackoff(
|
|
277
|
+
() => fetchUser('123'),
|
|
278
|
+
maxAttempts: 5,
|
|
279
|
+
);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Circuit Breaker Pattern
|
|
283
|
+
|
|
284
|
+
```dart
|
|
285
|
+
// ✅ Prevent cascading failures
|
|
286
|
+
class CircuitBreaker {
|
|
287
|
+
final int failureThreshold;
|
|
288
|
+
final Duration timeout;
|
|
289
|
+
|
|
290
|
+
int _failureCount = 0;
|
|
291
|
+
DateTime? _openedAt;
|
|
292
|
+
|
|
293
|
+
CircuitBreaker({
|
|
294
|
+
this.failureThreshold = 5,
|
|
295
|
+
this.timeout = const Duration(minutes: 1),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
Future<T> execute<T>(Future<T> Function() operation) async {
|
|
299
|
+
if (_isOpen()) {
|
|
300
|
+
if (_shouldReset()) {
|
|
301
|
+
_reset();
|
|
302
|
+
} else {
|
|
303
|
+
throw AppException('Circuit breaker is open');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
final result = await operation();
|
|
309
|
+
_onSuccess();
|
|
310
|
+
return result;
|
|
311
|
+
} catch (e) {
|
|
312
|
+
_onFailure();
|
|
313
|
+
rethrow;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
bool _isOpen() => _openedAt != null;
|
|
318
|
+
|
|
319
|
+
bool _shouldReset() {
|
|
320
|
+
return _openedAt != null &&
|
|
321
|
+
DateTime.now().difference(_openedAt!) > timeout;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
void _onSuccess() {
|
|
325
|
+
_failureCount = 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
void _onFailure() {
|
|
329
|
+
_failureCount++;
|
|
330
|
+
if (_failureCount >= failureThreshold) {
|
|
331
|
+
_openedAt = DateTime.now();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
void _reset() {
|
|
336
|
+
_failureCount = 0;
|
|
337
|
+
_openedAt = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Best Practices
|
|
343
|
+
|
|
344
|
+
- Create custom exception classes for different error types
|
|
345
|
+
- Catch specific exceptions before general ones
|
|
346
|
+
- Use `rethrow` to preserve stack traces
|
|
347
|
+
- Always clean up resources in `finally` blocks
|
|
348
|
+
- Validate input early and throw meaningful errors
|
|
349
|
+
- Use Result/Either types for expected failures
|
|
350
|
+
- Implement global error handlers for uncaught errors
|
|
351
|
+
- Use retry with exponential backoff for transient failures
|
|
352
|
+
- Implement circuit breakers for external service calls
|
|
353
|
+
- Don't catch exceptions you can't handle
|
|
354
|
+
- Include context in error messages
|
|
355
|
+
- Log errors with stack traces
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Dart Language Guidelines
|
|
2
|
+
|
|
3
|
+
Comprehensive guidelines for writing idiomatic Dart code.
|
|
4
|
+
|
|
5
|
+
## Topics
|
|
6
|
+
|
|
7
|
+
- [Fundamentals](basics.md) - Types, null safety, syntax
|
|
8
|
+
- [Async Programming](async.md) - Futures, async/await, Streams
|
|
9
|
+
- [Error Handling](error-handling.md) - Exceptions and error patterns
|
|
10
|
+
- [Testing](testing.md) - Unit and widget testing
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Testing in Dart
|
|
2
|
+
|
|
3
|
+
## Unit Testing
|
|
4
|
+
|
|
5
|
+
```dart
|
|
6
|
+
import 'package:test/test.dart';
|
|
7
|
+
|
|
8
|
+
// ✅ Arrange-Act-Assert pattern
|
|
9
|
+
void main() {
|
|
10
|
+
group('UserService', () {
|
|
11
|
+
test('should create user with hashed password', () async {
|
|
12
|
+
// Arrange
|
|
13
|
+
final repository = MockUserRepository();
|
|
14
|
+
final service = UserService(repository);
|
|
15
|
+
final userData = UserData(email: 'test@example.com', password: 'secret');
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
final user = await service.createUser(userData);
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(user.email, equals('test@example.com'));
|
|
22
|
+
expect(user.passwordHash, isNot(equals('secret')));
|
|
23
|
+
verify(repository.save(any)).called(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should throw ValidationException for invalid email', () {
|
|
27
|
+
final service = UserService(MockUserRepository());
|
|
28
|
+
|
|
29
|
+
expect(
|
|
30
|
+
() => service.createUser(UserData(email: 'invalid', password: 'pass')),
|
|
31
|
+
throwsA(isA<ValidationException>()),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Test Organization
|
|
39
|
+
|
|
40
|
+
```dart
|
|
41
|
+
void main() {
|
|
42
|
+
// ✅ Use group for related tests
|
|
43
|
+
group('Calculator', () {
|
|
44
|
+
late Calculator calculator;
|
|
45
|
+
|
|
46
|
+
// setUp runs before each test
|
|
47
|
+
setUp(() {
|
|
48
|
+
calculator = Calculator();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// tearDown runs after each test
|
|
52
|
+
tearDown(() {
|
|
53
|
+
calculator.dispose();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('adds two numbers', () {
|
|
57
|
+
expect(calculator.add(2, 3), equals(5));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('subtracts two numbers', () {
|
|
61
|
+
expect(calculator.subtract(5, 3), equals(2));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
group('division', () {
|
|
65
|
+
test('divides two numbers', () {
|
|
66
|
+
expect(calculator.divide(10, 2), equals(5));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('throws on division by zero', () {
|
|
70
|
+
expect(
|
|
71
|
+
() => calculator.divide(10, 0),
|
|
72
|
+
throwsA(isA<DivisionByZeroException>()),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Matchers
|
|
81
|
+
|
|
82
|
+
```dart
|
|
83
|
+
// Equality
|
|
84
|
+
expect(value, equals(expected));
|
|
85
|
+
expect(value, isNot(equals(unexpected)));
|
|
86
|
+
|
|
87
|
+
// Types
|
|
88
|
+
expect(value, isA<String>());
|
|
89
|
+
expect(value, isNotNull);
|
|
90
|
+
expect(value, isNull);
|
|
91
|
+
|
|
92
|
+
// Numbers
|
|
93
|
+
expect(value, greaterThan(5));
|
|
94
|
+
expect(value, lessThan(10));
|
|
95
|
+
expect(value, closeTo(3.14, 0.01));
|
|
96
|
+
|
|
97
|
+
// Strings
|
|
98
|
+
expect(text, contains('hello'));
|
|
99
|
+
expect(text, startsWith('Hello'));
|
|
100
|
+
expect(text, endsWith('world'));
|
|
101
|
+
expect(text, matches(r'^\d+$'));
|
|
102
|
+
|
|
103
|
+
// Collections
|
|
104
|
+
expect(list, isEmpty);
|
|
105
|
+
expect(list, isNotEmpty);
|
|
106
|
+
expect(list, hasLength(3));
|
|
107
|
+
expect(list, contains(item));
|
|
108
|
+
expect(map, containsValue(value));
|
|
109
|
+
|
|
110
|
+
// Futures
|
|
111
|
+
expect(future, completes);
|
|
112
|
+
expect(future, throwsException);
|
|
113
|
+
expect(future, completion(equals(value)));
|
|
114
|
+
|
|
115
|
+
// Custom matchers
|
|
116
|
+
expect(user, isA<User>().having((u) => u.email, 'email', 'test@example.com'));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Mocking with Mockito
|
|
120
|
+
|
|
121
|
+
```dart
|
|
122
|
+
import 'package:mockito/mockito.dart';
|
|
123
|
+
import 'package:mockito/annotations.dart';
|
|
124
|
+
|
|
125
|
+
// Generate mocks
|
|
126
|
+
@GenerateMocks([UserRepository, ApiClient])
|
|
127
|
+
void main() {
|
|
128
|
+
group('UserService', () {
|
|
129
|
+
late MockUserRepository repository;
|
|
130
|
+
late UserService service;
|
|
131
|
+
|
|
132
|
+
setUp(() {
|
|
133
|
+
repository = MockUserRepository();
|
|
134
|
+
service = UserService(repository);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should fetch user by id', () async {
|
|
138
|
+
// Arrange - stub method
|
|
139
|
+
final expectedUser = User(id: '1', name: 'Alice');
|
|
140
|
+
when(repository.findById('1')).thenAnswer((_) async => expectedUser);
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
final user = await service.getUser('1');
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(user, equals(expectedUser));
|
|
147
|
+
verify(repository.findById('1')).called(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should handle not found', () async {
|
|
151
|
+
// Stub to return null
|
|
152
|
+
when(repository.findById(any)).thenAnswer((_) async => null);
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
() => service.getUser('999'),
|
|
156
|
+
throwsA(isA<NotFoundException>()),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Testing Async Code
|
|
164
|
+
|
|
165
|
+
```dart
|
|
166
|
+
test('should fetch data asynchronously', () async {
|
|
167
|
+
final data = await fetchData();
|
|
168
|
+
expect(data, isNotNull);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should complete within timeout', () async {
|
|
172
|
+
await expectLater(
|
|
173
|
+
slowOperation().timeout(const Duration(seconds: 2)),
|
|
174
|
+
completes,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('should handle errors', () {
|
|
179
|
+
expect(
|
|
180
|
+
failingOperation(),
|
|
181
|
+
throwsA(isA<NetworkException>()),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Testing Streams
|
|
187
|
+
|
|
188
|
+
```dart
|
|
189
|
+
test('should emit values from stream', () async {
|
|
190
|
+
final stream = Stream.fromIterable([1, 2, 3]);
|
|
191
|
+
|
|
192
|
+
expect(stream, emitsInOrder([1, 2, 3]));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should emit and complete', () {
|
|
196
|
+
expect(
|
|
197
|
+
countStream(3),
|
|
198
|
+
emitsInOrder([
|
|
199
|
+
1,
|
|
200
|
+
2,
|
|
201
|
+
3,
|
|
202
|
+
emitsDone,
|
|
203
|
+
]),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('should handle stream errors', () {
|
|
208
|
+
expect(
|
|
209
|
+
errorStream(),
|
|
210
|
+
emitsInOrder([
|
|
211
|
+
1,
|
|
212
|
+
emitsError(isA<Exception>()),
|
|
213
|
+
]),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Widget Testing (Flutter)
|
|
219
|
+
|
|
220
|
+
```dart
|
|
221
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
222
|
+
|
|
223
|
+
void main() {
|
|
224
|
+
testWidgets('should display user name', (WidgetTester tester) async {
|
|
225
|
+
// Arrange
|
|
226
|
+
final user = User(id: '1', name: 'Alice');
|
|
227
|
+
|
|
228
|
+
// Act
|
|
229
|
+
await tester.pumpWidget(
|
|
230
|
+
MaterialApp(home: UserProfile(user: user)),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Assert
|
|
234
|
+
expect(find.text('Alice'), findsOneWidget);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
testWidgets('should handle button tap', (WidgetTester tester) async {
|
|
238
|
+
bool tapped = false;
|
|
239
|
+
|
|
240
|
+
await tester.pumpWidget(
|
|
241
|
+
MaterialApp(
|
|
242
|
+
home: Scaffold(
|
|
243
|
+
body: ElevatedButton(
|
|
244
|
+
onPressed: () => tapped = true,
|
|
245
|
+
child: const Text('Tap me'),
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Find and tap button
|
|
252
|
+
await tester.tap(find.text('Tap me'));
|
|
253
|
+
await tester.pump();
|
|
254
|
+
|
|
255
|
+
expect(tapped, isTrue);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Integration Testing
|
|
261
|
+
|
|
262
|
+
```dart
|
|
263
|
+
import 'package:integration_test/integration_test.dart';
|
|
264
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
265
|
+
|
|
266
|
+
void main() {
|
|
267
|
+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
268
|
+
|
|
269
|
+
group('Login flow', () {
|
|
270
|
+
testWidgets('should login successfully', (tester) async {
|
|
271
|
+
await tester.pumpWidget(MyApp());
|
|
272
|
+
|
|
273
|
+
// Enter credentials
|
|
274
|
+
await tester.enterText(
|
|
275
|
+
find.byKey(const Key('email_field')),
|
|
276
|
+
'test@example.com',
|
|
277
|
+
);
|
|
278
|
+
await tester.enterText(
|
|
279
|
+
find.byKey(const Key('password_field')),
|
|
280
|
+
'password123',
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Tap login
|
|
284
|
+
await tester.tap(find.byKey(const Key('login_button')));
|
|
285
|
+
await tester.pumpAndSettle();
|
|
286
|
+
|
|
287
|
+
// Verify navigation to home
|
|
288
|
+
expect(find.byType(HomePage), findsOneWidget);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Test Doubles
|
|
295
|
+
|
|
296
|
+
```dart
|
|
297
|
+
// Fake - working implementation (not for production)
|
|
298
|
+
class FakeUserRepository implements UserRepository {
|
|
299
|
+
final _users = <String, User>{};
|
|
300
|
+
|
|
301
|
+
@override
|
|
302
|
+
Future<User?> findById(String id) async {
|
|
303
|
+
return _users[id];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@override
|
|
307
|
+
Future<void> save(User user) async {
|
|
308
|
+
_users[user.id] = user;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Stub - returns canned responses
|
|
313
|
+
class StubApiClient implements ApiClient {
|
|
314
|
+
@override
|
|
315
|
+
Future<Response> get(String url) async {
|
|
316
|
+
return Response(statusCode: 200, data: {'id': '1', 'name': 'Alice'});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Mock - pre-programmed with expectations (use Mockito)
|
|
321
|
+
final mock = MockUserRepository();
|
|
322
|
+
when(mock.findById('1')).thenAnswer((_) async => testUser);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Coverage
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Run tests with coverage
|
|
329
|
+
dart test --coverage=coverage
|
|
330
|
+
|
|
331
|
+
# Generate HTML report
|
|
332
|
+
genhtml coverage/lcov.info -o coverage/html
|
|
333
|
+
|
|
334
|
+
# View coverage
|
|
335
|
+
open coverage/html/index.html
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Best Practices
|
|
339
|
+
|
|
340
|
+
- Use `group()` to organize related tests
|
|
341
|
+
- One test per behavior
|
|
342
|
+
- Use descriptive test names: "should [expected behavior] when [scenario]"
|
|
343
|
+
- Follow Arrange-Act-Assert pattern
|
|
344
|
+
- Test observable behavior, not implementation
|
|
345
|
+
- Use setUp/tearDown for common initialization
|
|
346
|
+
- Mock external dependencies
|
|
347
|
+
- Test edge cases and error conditions
|
|
348
|
+
- Keep tests fast and independent
|
|
349
|
+
- Use `pump()` and `pumpAndSettle()` in widget tests
|
|
350
|
+
- Verify interactions with mocks
|
|
351
|
+
- Aim for high code coverage (80%+)
|
|
352
|
+
- Run tests in CI/CD pipeline
|