test: 통합 테스트 오류 및 경고 수정
- 모든 서비스 메서드 시그니처를 실제 구현에 맞게 수정 - TestDataGenerator 제거하고 직접 객체 생성으로 변경 - 모델 필드명 및 타입 불일치 수정 - 불필요한 Either 패턴 사용 제거 - null safety 관련 이슈 해결 수정된 파일: - test/integration/screens/company_integration_test.dart - test/integration/screens/equipment_integration_test.dart - test/integration/screens/user_integration_test.dart - test/integration/screens/login_integration_test.dart
This commit is contained in:
531
test/integration/automated/framework/testable_action.dart
Normal file
531
test/integration/automated/framework/testable_action.dart
Normal file
@@ -0,0 +1,531 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// 테스트 가능한 액션의 기본 인터페이스
|
||||
abstract class TestableAction {
|
||||
/// 액션 이름
|
||||
String get name;
|
||||
|
||||
/// 액션 설명
|
||||
String get description;
|
||||
|
||||
/// 액션 실행 전 조건 검증
|
||||
Future<bool> canExecute(WidgetTester tester);
|
||||
|
||||
/// 액션 실행
|
||||
Future<ActionResult> execute(WidgetTester tester);
|
||||
|
||||
/// 액션 실행 후 검증
|
||||
Future<bool> verify(WidgetTester tester);
|
||||
|
||||
/// 에러 발생 시 복구 시도
|
||||
Future<bool> recover(WidgetTester tester, dynamic error);
|
||||
}
|
||||
|
||||
/// 액션 실행 결과
|
||||
class ActionResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final dynamic data;
|
||||
final Duration executionTime;
|
||||
final Map<String, dynamic>? metrics;
|
||||
final dynamic error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
ActionResult({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
required this.executionTime,
|
||||
this.metrics,
|
||||
this.error,
|
||||
this.stackTrace,
|
||||
});
|
||||
|
||||
factory ActionResult.success({
|
||||
String? message,
|
||||
dynamic data,
|
||||
required Duration executionTime,
|
||||
Map<String, dynamic>? metrics,
|
||||
}) {
|
||||
return ActionResult(
|
||||
success: true,
|
||||
message: message,
|
||||
data: data,
|
||||
executionTime: executionTime,
|
||||
metrics: metrics,
|
||||
);
|
||||
}
|
||||
|
||||
factory ActionResult.failure({
|
||||
required String message,
|
||||
required Duration executionTime,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
return ActionResult(
|
||||
success: false,
|
||||
message: message,
|
||||
executionTime: executionTime,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 기본 테스트 액션 구현
|
||||
abstract class BaseTestableAction implements TestableAction {
|
||||
@override
|
||||
Future<bool> canExecute(WidgetTester tester) async {
|
||||
// 기본적으로 항상 실행 가능
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verify(WidgetTester tester) async {
|
||||
// 기본 검증은 성공으로 가정
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> recover(WidgetTester tester, dynamic error) async {
|
||||
// 기본 복구는 실패로 가정
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 탭 액션
|
||||
class TapAction extends BaseTestableAction {
|
||||
final Finder finder;
|
||||
final String targetName;
|
||||
|
||||
TapAction({
|
||||
required this.finder,
|
||||
required this.targetName,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Tap $targetName';
|
||||
|
||||
@override
|
||||
String get description => 'Tap on $targetName';
|
||||
|
||||
@override
|
||||
Future<bool> canExecute(WidgetTester tester) async {
|
||||
return finder.evaluate().isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
await tester.tap(finder);
|
||||
await tester.pump();
|
||||
|
||||
return ActionResult.success(
|
||||
message: 'Successfully tapped $targetName',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Failed to tap $targetName: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 텍스트 입력 액션
|
||||
class EnterTextAction extends BaseTestableAction {
|
||||
final Finder finder;
|
||||
final String text;
|
||||
final String fieldName;
|
||||
|
||||
EnterTextAction({
|
||||
required this.finder,
|
||||
required this.text,
|
||||
required this.fieldName,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Enter text in $fieldName';
|
||||
|
||||
@override
|
||||
String get description => 'Enter "$text" in $fieldName field';
|
||||
|
||||
@override
|
||||
Future<bool> canExecute(WidgetTester tester) async {
|
||||
return finder.evaluate().isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
await tester.enterText(finder, text);
|
||||
await tester.pump();
|
||||
|
||||
return ActionResult.success(
|
||||
message: 'Successfully entered text in $fieldName',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Failed to enter text in $fieldName: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 대기 액션
|
||||
class WaitAction extends BaseTestableAction {
|
||||
final Duration duration;
|
||||
final String? reason;
|
||||
|
||||
WaitAction({
|
||||
required this.duration,
|
||||
this.reason,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Wait ${duration.inMilliseconds}ms';
|
||||
|
||||
@override
|
||||
String get description => reason ?? 'Wait for ${duration.inMilliseconds}ms';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
await tester.pump(duration);
|
||||
|
||||
return ActionResult.success(
|
||||
message: 'Waited for ${duration.inMilliseconds}ms',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Failed to wait: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 스크롤 액션
|
||||
class ScrollAction extends BaseTestableAction {
|
||||
final Finder scrollable;
|
||||
final Finder? target;
|
||||
final Offset offset;
|
||||
final int maxAttempts;
|
||||
|
||||
ScrollAction({
|
||||
required this.scrollable,
|
||||
this.target,
|
||||
this.offset = const Offset(0, -300),
|
||||
this.maxAttempts = 10,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Scroll';
|
||||
|
||||
@override
|
||||
String get description => target != null
|
||||
? 'Scroll to find target widget'
|
||||
: 'Scroll by offset ${offset.dx}, ${offset.dy}';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
if (target != null) {
|
||||
// 타겟을 찾을 때까지 스크롤
|
||||
for (int i = 0; i < maxAttempts; i++) {
|
||||
if (target!.evaluate().isNotEmpty) {
|
||||
return ActionResult.success(
|
||||
message: 'Found target after $i scrolls',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.drag(scrollable, offset);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
return ActionResult.failure(
|
||||
message: 'Target not found after $maxAttempts scrolls',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
} else {
|
||||
// 단순 스크롤
|
||||
await tester.drag(scrollable, offset);
|
||||
await tester.pump();
|
||||
|
||||
return ActionResult.success(
|
||||
message: 'Scrolled by offset',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Failed to scroll: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 검증 액션
|
||||
class VerifyAction extends BaseTestableAction {
|
||||
final Future<bool> Function(WidgetTester) verifyFunction;
|
||||
final String verificationName;
|
||||
|
||||
VerifyAction({
|
||||
required this.verifyFunction,
|
||||
required this.verificationName,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Verify $verificationName';
|
||||
|
||||
@override
|
||||
String get description => 'Verify that $verificationName';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final result = await verifyFunction(tester);
|
||||
|
||||
if (result) {
|
||||
return ActionResult.success(
|
||||
message: 'Verification passed: $verificationName',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
} else {
|
||||
return ActionResult.failure(
|
||||
message: 'Verification failed: $verificationName',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Verification error: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 복합 액션 (여러 액션을 순차적으로 실행)
|
||||
class CompositeAction extends BaseTestableAction {
|
||||
final List<TestableAction> actions;
|
||||
final String compositeName;
|
||||
final bool stopOnFailure;
|
||||
|
||||
CompositeAction({
|
||||
required this.actions,
|
||||
required this.compositeName,
|
||||
this.stopOnFailure = true,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => compositeName;
|
||||
|
||||
@override
|
||||
String get description => 'Execute ${actions.length} actions for $compositeName';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final results = <ActionResult>[];
|
||||
|
||||
for (final action in actions) {
|
||||
if (!await action.canExecute(tester)) {
|
||||
if (stopOnFailure) {
|
||||
return ActionResult.failure(
|
||||
message: 'Cannot execute action: ${action.name}',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final result = await action.execute(tester);
|
||||
results.add(result);
|
||||
|
||||
if (!result.success && stopOnFailure) {
|
||||
return ActionResult.failure(
|
||||
message: 'Failed at action: ${action.name} - ${result.message}',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: result.error,
|
||||
stackTrace: result.stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
if (!await action.verify(tester) && stopOnFailure) {
|
||||
return ActionResult.failure(
|
||||
message: 'Verification failed for action: ${action.name}',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final successCount = results.where((r) => r.success).length;
|
||||
final totalCount = results.length;
|
||||
|
||||
return ActionResult.success(
|
||||
message: 'Completed $successCount/$totalCount actions successfully',
|
||||
data: results,
|
||||
executionTime: stopwatch.elapsed,
|
||||
metrics: {
|
||||
'total_actions': totalCount,
|
||||
'successful_actions': successCount,
|
||||
'failed_actions': totalCount - successCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 조건부 액션
|
||||
class ConditionalAction extends BaseTestableAction {
|
||||
final Future<bool> Function(WidgetTester) condition;
|
||||
final TestableAction trueAction;
|
||||
final TestableAction? falseAction;
|
||||
final String conditionName;
|
||||
|
||||
ConditionalAction({
|
||||
required this.condition,
|
||||
required this.trueAction,
|
||||
this.falseAction,
|
||||
required this.conditionName,
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Conditional: $conditionName';
|
||||
|
||||
@override
|
||||
String get description => 'Execute action based on condition: $conditionName';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final conditionMet = await condition(tester);
|
||||
|
||||
if (conditionMet) {
|
||||
final result = await trueAction.execute(tester);
|
||||
return ActionResult(
|
||||
success: result.success,
|
||||
message: 'Condition met - ${result.message}',
|
||||
data: result.data,
|
||||
executionTime: stopwatch.elapsed,
|
||||
metrics: result.metrics,
|
||||
error: result.error,
|
||||
stackTrace: result.stackTrace,
|
||||
);
|
||||
} else if (falseAction != null) {
|
||||
final result = await falseAction!.execute(tester);
|
||||
return ActionResult(
|
||||
success: result.success,
|
||||
message: 'Condition not met - ${result.message}',
|
||||
data: result.data,
|
||||
executionTime: stopwatch.elapsed,
|
||||
metrics: result.metrics,
|
||||
error: result.error,
|
||||
stackTrace: result.stackTrace,
|
||||
);
|
||||
} else {
|
||||
return ActionResult.success(
|
||||
message: 'Condition not met - no action taken',
|
||||
executionTime: stopwatch.elapsed,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
return ActionResult.failure(
|
||||
message: 'Conditional action error: $e',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 재시도 액션
|
||||
class RetryAction extends BaseTestableAction {
|
||||
final TestableAction action;
|
||||
final int maxRetries;
|
||||
final Duration retryDelay;
|
||||
|
||||
RetryAction({
|
||||
required this.action,
|
||||
this.maxRetries = 3,
|
||||
this.retryDelay = const Duration(seconds: 1),
|
||||
});
|
||||
|
||||
@override
|
||||
String get name => 'Retry ${action.name}';
|
||||
|
||||
@override
|
||||
String get description => 'Retry ${action.name} up to $maxRetries times';
|
||||
|
||||
@override
|
||||
Future<ActionResult> execute(WidgetTester tester) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
ActionResult? lastResult;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
if (!await action.canExecute(tester)) {
|
||||
await tester.pump(retryDelay);
|
||||
continue;
|
||||
}
|
||||
|
||||
lastResult = await action.execute(tester);
|
||||
|
||||
if (lastResult.success) {
|
||||
return ActionResult.success(
|
||||
message: 'Succeeded on attempt $attempt - ${lastResult.message}',
|
||||
data: lastResult.data,
|
||||
executionTime: stopwatch.elapsed,
|
||||
metrics: {
|
||||
...?lastResult.metrics,
|
||||
'attempts': attempt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await tester.pump(retryDelay);
|
||||
|
||||
// 복구 시도
|
||||
if (lastResult.error != null) {
|
||||
await action.recover(tester, lastResult.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ActionResult.failure(
|
||||
message: 'Failed after $maxRetries attempts - ${lastResult?.message}',
|
||||
executionTime: stopwatch.elapsed,
|
||||
error: lastResult?.error,
|
||||
stackTrace: lastResult?.stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user