Files
2025-10-29 00:25:03 +09:00
..
2025-10-29 00:25:03 +09:00

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 반영 정도만 최소 구현했습니다.

사용 예시:

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
  • 시드 이벤트
    • seededEventsEvent 리스트를 주입해 목록을 렌더링합니다.
  • 안정화 포인트
    • createEvent/cloneEvent는 notifyListeners 후 바로 반환합니다.

사용 예시:

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 시나리오용으로 제공되며, 필요 시 확장하세요.

사용 예시:

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)를 추가합니다.

샘플 패턴:

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 사용 여부 등) 확인.