- 테스트 패키지 추가 (mockito, golden_toolkit, patrol 등) - 테스트 가이드 문서 작성 (TEST_GUIDE.md) - 테스트 진행 상황 문서 작성 (TEST_PROGRESS.md) - 테스트 헬퍼 클래스 구현 - test_helpers.dart: 기본 테스트 유틸리티 - mock_data_helpers.dart: Mock 데이터 생성 헬퍼 - mock_services.dart: Mock 서비스 설정 (오류 수정 완료) - simple_mock_services.dart: 간단한 Mock 서비스 - 단위 테스트 구현 - CompanyListController 테스트 - EquipmentListController 테스트 - UserListController 테스트 - Widget 테스트 구현 (CompanyListScreen) Mock 서비스 주요 수정사항: - dartz import 추가 - Either 타입 제거 (실제 서비스와 일치하도록) - 메서드 시그니처 수정 (실제 서비스 인터페이스와 일치) - Mock 데이터 생성 메서드 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
296 lines
7.5 KiB
Dart
296 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
/// 테스트용 GetIt 인스턴스 초기화
|
|
GetIt setupTestGetIt() {
|
|
final getIt = GetIt.instance;
|
|
|
|
// 기존 등록된 서비스들 모두 제거
|
|
getIt.reset();
|
|
|
|
return getIt;
|
|
}
|
|
|
|
/// 테스트용 위젯 래퍼
|
|
/// 모든 위젯 테스트에서 필요한 기본 설정을 제공
|
|
class TestWidgetWrapper extends StatelessWidget {
|
|
final Widget child;
|
|
final List<ChangeNotifierProvider>? providers;
|
|
final NavigatorObserver? navigatorObserver;
|
|
final Map<String, WidgetBuilder>? routes;
|
|
final String? initialRoute;
|
|
|
|
const TestWidgetWrapper({
|
|
Key? key,
|
|
required this.child,
|
|
this.providers,
|
|
this.navigatorObserver,
|
|
this.routes,
|
|
this.initialRoute,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget wrappedChild = MaterialApp(
|
|
title: 'Test App',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue,
|
|
useMaterial3: true,
|
|
),
|
|
localizationsDelegates: const [
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
supportedLocales: const [
|
|
Locale('ko', 'KR'),
|
|
Locale('en', 'US'),
|
|
],
|
|
home: Scaffold(body: child),
|
|
routes: routes ?? {},
|
|
initialRoute: initialRoute,
|
|
navigatorObservers: navigatorObserver != null ? [navigatorObserver!] : [],
|
|
);
|
|
|
|
// Provider가 있는 경우 래핑
|
|
if (providers != null && providers!.isNotEmpty) {
|
|
return MultiProvider(
|
|
providers: providers!,
|
|
child: wrappedChild,
|
|
);
|
|
}
|
|
|
|
return wrappedChild;
|
|
}
|
|
}
|
|
|
|
/// 위젯을 테스트 환경에서 펌프하는 헬퍼 함수
|
|
Future<void> pumpTestWidget(
|
|
WidgetTester tester,
|
|
Widget widget, {
|
|
List<ChangeNotifierProvider>? providers,
|
|
NavigatorObserver? navigatorObserver,
|
|
Map<String, WidgetBuilder>? routes,
|
|
String? initialRoute,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
TestWidgetWrapper(
|
|
child: widget,
|
|
providers: providers,
|
|
navigatorObserver: navigatorObserver,
|
|
routes: routes,
|
|
initialRoute: initialRoute,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 비동기 작업을 기다리고 위젯을 리빌드하는 헬퍼
|
|
Future<void> pumpAndSettleWithTimeout(
|
|
WidgetTester tester, {
|
|
Duration timeout = const Duration(seconds: 10),
|
|
}) async {
|
|
await tester.pump();
|
|
await tester.pumpAndSettle(timeout);
|
|
}
|
|
|
|
/// TextField에 텍스트를 입력하는 헬퍼
|
|
Future<void> enterTextByLabel(
|
|
WidgetTester tester,
|
|
String label,
|
|
String text,
|
|
) async {
|
|
final textFieldFinder = find.ancestor(
|
|
of: find.text(label),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
if (textFieldFinder.evaluate().isEmpty) {
|
|
// 라벨로 찾지 못한 경우, 가까운 TextFormField 찾기
|
|
final labelWidget = find.text(label);
|
|
final textField = find.byType(TextFormField).first;
|
|
await tester.enterText(textField, text);
|
|
} else {
|
|
await tester.enterText(textFieldFinder, text);
|
|
}
|
|
}
|
|
|
|
/// 버튼을 찾고 탭하는 헬퍼
|
|
Future<void> tapButtonByText(
|
|
WidgetTester tester,
|
|
String buttonText,
|
|
) async {
|
|
final buttonFinder = find.widgetWithText(ElevatedButton, buttonText);
|
|
|
|
if (buttonFinder.evaluate().isEmpty) {
|
|
// ElevatedButton이 아닌 경우 다른 버튼 타입 시도
|
|
final textButtonFinder = find.widgetWithText(TextButton, buttonText);
|
|
if (textButtonFinder.evaluate().isNotEmpty) {
|
|
await tester.tap(textButtonFinder);
|
|
return;
|
|
}
|
|
|
|
final outlinedButtonFinder = find.widgetWithText(OutlinedButton, buttonText);
|
|
if (outlinedButtonFinder.evaluate().isNotEmpty) {
|
|
await tester.tap(outlinedButtonFinder);
|
|
return;
|
|
}
|
|
|
|
// 아무 버튼도 찾지 못한 경우 텍스트만으로 시도
|
|
await tester.tap(find.text(buttonText));
|
|
} else {
|
|
await tester.tap(buttonFinder);
|
|
}
|
|
}
|
|
|
|
/// 스낵바 메시지 검증 헬퍼
|
|
void expectSnackBar(WidgetTester tester, String message) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(SnackBar),
|
|
matching: find.text(message),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
|
|
/// 로딩 인디케이터 검증 헬퍼
|
|
void expectLoading(WidgetTester tester, {bool isLoading = true}) {
|
|
expect(
|
|
find.byType(CircularProgressIndicator),
|
|
isLoading ? findsOneWidget : findsNothing,
|
|
);
|
|
}
|
|
|
|
/// 에러 메시지 검증 헬퍼
|
|
void expectErrorMessage(WidgetTester tester, String errorMessage) {
|
|
expect(find.text(errorMessage), findsOneWidget);
|
|
}
|
|
|
|
/// 화면 전환 대기 헬퍼
|
|
Future<void> waitForNavigation(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300)); // 애니메이션 대기
|
|
}
|
|
|
|
/// 다이얼로그 검증 헬퍼
|
|
void expectDialog(WidgetTester tester, {String? title, String? content}) {
|
|
expect(find.byType(Dialog), findsOneWidget);
|
|
|
|
if (title != null) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(Dialog),
|
|
matching: find.text(title),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
|
|
if (content != null) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(Dialog),
|
|
matching: find.text(content),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 다이얼로그 닫기 헬퍼
|
|
Future<void> closeDialog(WidgetTester tester) async {
|
|
// 다이얼로그 외부 탭하여 닫기
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pump();
|
|
}
|
|
|
|
/// 스크롤하여 위젯 찾기 헬퍼
|
|
Future<void> scrollUntilVisible(
|
|
WidgetTester tester,
|
|
Finder finder, {
|
|
double delta = 300,
|
|
int maxScrolls = 10,
|
|
Finder? scrollable,
|
|
}) async {
|
|
final scrollableFinder = scrollable ?? find.byType(Scrollable).first;
|
|
|
|
for (int i = 0; i < maxScrolls; i++) {
|
|
if (finder.evaluate().isNotEmpty) {
|
|
return;
|
|
}
|
|
|
|
await tester.drag(scrollableFinder, Offset(0, -delta));
|
|
await tester.pump();
|
|
}
|
|
}
|
|
|
|
/// 테이블이나 리스트에서 특정 행 찾기 헬퍼
|
|
Finder findRowContaining(String text) {
|
|
return find.ancestor(
|
|
of: find.text(text),
|
|
matching: find.byType(Row),
|
|
);
|
|
}
|
|
|
|
/// 폼 필드 검증 헬퍼
|
|
void expectFormFieldError(WidgetTester tester, String fieldLabel, String errorText) {
|
|
final formField = find.ancestor(
|
|
of: find.text(fieldLabel),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
final errorFinder = find.descendant(
|
|
of: formField,
|
|
matching: find.text(errorText),
|
|
);
|
|
|
|
expect(errorFinder, findsOneWidget);
|
|
}
|
|
|
|
/// 드롭다운 선택 헬퍼
|
|
Future<void> selectDropdownItem(
|
|
WidgetTester tester,
|
|
String dropdownLabel,
|
|
String itemText,
|
|
) async {
|
|
// 드롭다운 찾기
|
|
final dropdown = find.ancestor(
|
|
of: find.text(dropdownLabel),
|
|
matching: find.byType(DropdownButtonFormField),
|
|
);
|
|
|
|
// 드롭다운 열기
|
|
await tester.tap(dropdown);
|
|
await tester.pump();
|
|
|
|
// 아이템 선택
|
|
await tester.tap(find.text(itemText).last);
|
|
await tester.pump();
|
|
}
|
|
|
|
/// 날짜 선택 헬퍼
|
|
Future<void> selectDate(
|
|
WidgetTester tester,
|
|
String dateFieldLabel,
|
|
DateTime date,
|
|
) async {
|
|
// 날짜 필드 탭
|
|
final dateField = find.ancestor(
|
|
of: find.text(dateFieldLabel),
|
|
matching: find.byType(TextFormField),
|
|
);
|
|
|
|
await tester.tap(dateField);
|
|
await tester.pump();
|
|
|
|
// 날짜 선택 (간단한 구현, 실제로는 더 복잡할 수 있음)
|
|
await tester.tap(find.text(date.day.toString()));
|
|
await tester.pump();
|
|
|
|
// 확인 버튼 탭
|
|
await tester.tap(find.text('확인'));
|
|
await tester.pump();
|
|
} |