Files
superport/test/integration/automated/framework/testable_action.dart
JiWoong Sul 198aac6525
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
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
2025-08-05 20:24:05 +09:00

531 lines
13 KiB
Dart

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,
);
}
}