# Test Utilities Guide

본 문서는 Flutter 위젯/서비스 테스트를 안정적으로 작성하기 위한 공용 유틸 사용 가이드를 제공합니다. 실제 예시는 프로젝트 내 테스트 코드에서 그대로 복사해 사용할 수 있습니다.

## 구성 요소

- test/utils/mock_services.dart
- test/utils/stub_event_service.dart
- test/utils/provider_presets.dart
- test/utils/widget_test_utils.dart (향후 확장 시)
- test/utils/event_bus_test_utils.dart

## 공통 패턴

- __오버레이/네비 의존 제거__: 팝업/스낵바/네비게이션은 테스트에서 `pumpAndSettle`를 가로막는 대표 원인입니다. 가능하면 화면에 `@visibleForTesting` 훅(예: `invokeSaveForTest`, `invokeDuplicateForTest`)을 제공하여 내부 로직을 직접 호출하세요.
- __pumpAndSettle 지양__: 무한 대기 가능성이 있으므로, `await tester.pump()` + `await tester.pump(const Duration(milliseconds: 50))` 조합을 우선적으로 사용합니다.
- __ensureVisible__: 탭/토글/입력 전에는 `await tester.ensureVisible(finder)`로 화면 내 가시성을 보장합니다.
- __Key 부여__: 테스트가 찾는 주요 위젯에는 `ValueKey`를 부여합니다. 예) `ValueKey('access_password_field')`, `ValueKey('event_menu_<id>')`.
- __SharedPreferences 모킹__: 영속 값(검색어 등)을 사용 시 `SharedPreferences.setMockInitialValues({...})`를 통해 초기 상태를 정의합니다.

## Mock/Stub 소개

### MockAuthService / MockClubService (`test/utils/mock_services.dart`)
- __MockAuthService__
  - 고정 토큰과 사용자 정보(`User`)를 제공합니다.
  - `isAuthenticated`, `currentUser`, `token` 등을 간단히 반환합니다.
- __MockClubService__
  - 단일 `Club`을 보유하고 `currentClub`/`clubs`를 제공합니다.
  - `updateClub()`는 `ownerId` 반영 정도만 최소 구현했습니다.

사용 예시:
```dart
final auth = MockAuthService(
  user: User(id: 'u1', email: 'u@ex.com', name: 'Tester', role: 'owner'),
  tokenValue: 'token',
);
final club = MockClubService(
  club: Club(id: 'club1', name: '클럽', averageCalculationPeriod: '3'),
);
```

### StubEventService (`test/utils/stub_event_service.dart`)
- 네트워크 호출을 모두 차단하고 즉시 완료되는 `EventService` 대체 구현입니다.
- __관찰 필드__
  - `lastCloneCalled`, `lastClonedEventId`, `lastCreatedEventData`
- __시드 이벤트__
  - `seededEvents`에 `Event` 리스트를 주입해 목록을 렌더링합니다.
- __안정화 포인트__
  - `createEvent/cloneEvent`는 notifyListeners 후 바로 반환합니다.

사용 예시:
```dart
final stub = StubEventService();
await stub.initialize('token');
stub.setClubId('club1');
stub.seededEvents = [Event(id: 'e1', clubId: 'club1', title: '타이틀', startDate: DateTime.now(), status: '활성', isActive: true)];
```

### Provider 프리셋 (`test/utils/provider_presets.dart`)
- 특정 화면에 필요한 Provider 집합을 손쉽게 구성합니다.
- 현재 예시는 ClubSettings 시나리오용으로 제공되며, 필요 시 확장하세요.

사용 예시:
```dart
MultiProvider(
  providers: [
    ChangeNotifierProvider<AuthService>.value(value: auth),
    ChangeNotifierProvider<ClubService>.value(value: club),
    ChangeNotifierProvider<EventService>.value(value: stub),
  ],
  child: const MaterialApp(home: EventsScreen()),
);
```

## 화면별 테스트 팁

### EventsScreen
- __복제 메뉴 테스트__: `PopupMenuButton` 오버레이를 열지 말고, 상태 훅 `invokeDuplicateForTest(event)`를 호출하세요.
- __ellipsis 회귀 테스트__: `Text` 위젯을 `find.textContaining()`으로 찾고, `Text.maxLines`/`Text.overflow`를 직접 검증하세요.

### EventFormScreen
- __저장 테스트__: 저장 버튼 탭 대신 `invokeSaveForTest()` 호출. 테스트 모드에서는 pop/스낵바를 우회하고 `_isLoading`을 해제하는 로직이 포함되어 있어 hang 방지에 유리합니다.
- __비밀번호 필드__: `ValueKey('access_password_field')`로 필드 접근, 토글 아이콘은 `Tooltip('비밀번호 표시')`/`('비밀번호 숨김')`으로 찾습니다.

## EventBus 테스트 유틸 (`test/utils/event_bus_test_utils.dart`)
- 각 테스트에서 고유 테스트 ID를 설정하고, setUp/tearDown에서 `EventBus`를 초기화/해제합니다.
- 간헐적 경합을 줄이기 위해 tearDown에서 짧은 대기(`Future.delayed`)를 추가합니다.

샘플 패턴:
```dart
setUp(() async {
  final testId = 'member_service_test_${DateTime.now().millisecondsSinceEpoch}';
  await EventBusTestUtils.setUp(testId);
});

tearDown(() async {
  await EventBusTestUtils.tearDown();
  await Future<void>.delayed(const Duration(milliseconds: 100));
});
```

## 권장 사항 요약

- __핵심__: 오버레이/네비 의존 제거, 테스트 훅 사용, `pumpAndSettle` 지양.
- __상태__: `_isLoading`은 성공/실패 모두 해제되도록 보장.
- __가시성__: 액션 전 `ensureVisible` 사용.
- __모델/타입__: 실제 모델 시그니처에 맞는 파라미터(문자열/정수, const 사용 여부 등) 확인.
