import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:mockito/annotations.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:lanebow/services/club_service.dart'; import 'package:lanebow/services/api_service.dart'; import 'package:lanebow/services/event_bus.dart'; import 'package:lanebow/models/club_model.dart'; import 'package:lanebow/config/api_config.dart'; import 'package:lanebow/utils/test_utils.dart'; import '../utils/event_bus_test_utils.dart'; // 모킹 클래스 생성 @GenerateMocks([ApiService]) import 'club_service_test.mocks.dart'; void main() { late ClubService clubService; late MockApiService mockApiService; final testToken = 'test_token'; final testClubId = 'test_club_id'; // 테스트 클럽 데이터 final testClub = { 'id': 'test_club_id', 'name': '테스트 클럽', 'description': '테스트 클럽 설명', 'location': '테스트 위치', 'ownerId': 'test_owner_id', 'createdAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(), 'memberCount': 10, 'isActive': true, }; setUp(() async { // 테스트 모드 강제 설정 TestUtils.setTestMode(true); // 테스트별 고유 ID 생성 및 설정 - 일관된 패턴 적용 final testId = 'club_service_test_${DateTime.now().millisecondsSinceEpoch}'; await EventBusTestUtils.setUp(testId); // SharedPreferences 모킹 SharedPreferences.setMockInitialValues({}); // API 서비스 모킹 mockApiService = MockApiService(); // ClubService 초기화 (테스트용 생성자 사용) clubService = ClubService.forTest(mockApiService); // 테스트용 토큰 초기화 await clubService.initialize(testToken); }); tearDown(() async { // 테스트 종료 후 정리 작업 try { // 서비스 정리 (한 번만 호출) clubService.dispose(); } catch (e) { // dispose 중 오류 무시 if (kDebugMode) { print('ClubService dispose 중 오류 무시: $e'); } } // EventBus 표준 정리 await EventBusTestUtils.tearDown(); // 테스트 모드 해제 TestUtils.setTestMode(false); // 추가 지연으로 비동기 작업 완료 대기 await Future.delayed(Duration(milliseconds: 100)); }); group('ClubService 테스트', () { test('initialize 메서드가 토큰을 설정해야 함', () async { // given // setUp에서 이미 initialize 호출됨 // then verify(mockApiService.setToken(testToken)).called(1); }); test('initialize 메서드가 저장된 클럽 ID가 있으면 클럽 정보를 가져와야 함', () async { // given SharedPreferences.setMockInitialValues({ ApiConfig.clubIdKey: testClubId, }); when(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); // when await clubService.initialize(testToken); // then verify(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).called(1); expect(clubService.currentClub?.id, testClubId); expect(clubService.currentClub?.name, '테스트 클럽'); }); test('fetchUserClubs 메서드가 사용자의 클럽 목록을 가져와야 함', () async { // given final testClubs = [testClub]; when(mockApiService.post('${ApiConfig.clubs}/user')) .thenAnswer((_) async => testClubs); // when await clubService.fetchUserClubs(); // then verify(mockApiService.post('${ApiConfig.clubs}/user')).called(1); expect(clubService.clubs.length, 1); expect(clubService.clubs[0].id, testClubId); expect(clubService.clubs[0].name, '테스트 클럽'); }); test('fetchClubById 메서드가 특정 클럽 정보를 가져와야 함', () async { // given when(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); // when await clubService.fetchClubById(testClubId); // then verify(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).called(1); expect(clubService.currentClub?.id, testClubId); expect(clubService.currentClub?.name, '테스트 클럽'); }); test('setCurrentClub 메서드가 클럽을 설정해야 함', () async { // given when(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); // when await clubService.setCurrentClub(testClubId); // then verify(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).called(1); expect(clubService.currentClub?.id, testClubId); }); test('selectClub 메서드가 클럽을 선택하고 이벤트를 발생시켜야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/select-club', data: {'clubId': testClubId}, )).thenAnswer((_) async => {}); when(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); // 이벤트 버스 테스트를 위한 변수 bool eventFired = false; String? eventClubId; StreamSubscription? subscription; try { // 이벤트 리스너 등록 - 리스너를 변수에 저장하여 나중에 확인 가능하게 함 subscription = EventBus().on().listen((event) { eventFired = true; eventClubId = event.clubId; }); // when await clubService.selectClub(testClubId); // 이벤트 처리를 위한 지연 추가 - 시간 증가 await Future.delayed(Duration(milliseconds: 500)); // then verify(mockApiService.post( '${ApiConfig.clubs}/select-club', data: {'clubId': testClubId}, )).called(1); verify(mockApiService.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).called(1); expect(clubService.currentClub?.id, testClubId); // 이벤트가 발생했는지 확인 expect(eventFired, true); expect(eventClubId, testClubId); } finally { // 구독 취소 - 반드시 실행되도록 finally 블록에 배치 await subscription?.cancel(); } }); test('createClub 메서드가 새 클럽을 생성해야 함', () async { // given final newClub = Club( name: '새 클럽', description: '새 클럽 설명', location: '새 위치', ); when(mockApiService.post( ApiConfig.clubs, data: newClub.toJson(), )).thenAnswer((_) async => {'club': testClub}); // when final result = await clubService.createClub(newClub); // then verify(mockApiService.post( ApiConfig.clubs, data: newClub.toJson(), )).called(1); expect(result.id, testClubId); expect(clubService.clubs.length, 1); expect(clubService.currentClub?.id, testClubId); }); test('updateClub 메서드가 클럽 정보를 업데이트해야 함', () async { // given - 테스트 로컬 인스턴스 사용으로 격리 final localMock = MockApiService(); final localService = ClubService.forTest(localMock); await localService.initialize(testToken); final updates = {'name': '업데이트된 클럽'}; final updatedClub = Map.from(testClub); updatedClub['name'] = '업데이트된 클럽'; // 클럽 목록/현재 클럽 설정은 로컬 서비스 기준으로 스텁 when(localMock.post('${ApiConfig.clubs}/user')) .thenAnswer((_) async => [testClub]); await localService.fetchUserClubs(); when(localMock.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); await localService.fetchClubById(testClubId); // 준비 단계 상호작용 초기화 clearInteractions(localMock); when(localMock.put( ApiConfig.clubs, data: { 'clubId': testClubId, ...updates, }, )).thenAnswer((_) async => {'club': updatedClub}); // when final result = await localService.updateClub(testClubId, updates); // 상태 수렴 보장 await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); // 추가 폴링: 이름 반영 수렴까지 대기 (최대 2.5s) const total = Duration(milliseconds: 2500); const step = Duration(milliseconds: 25); var waited = Duration.zero; while ( (localService.clubs.isEmpty || localService.clubs.first.name != '업데이트된 클럽' || localService.currentClub?.name != '업데이트된 클럽') && waited < total ) { await Future.delayed(step); waited += step; } // idle 보장 및 소폭 지연 추가로 최종 수렴 보장 await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); // then - 상태 중심 검증 expect(result.name, '업데이트된 클럽'); expect(localService.clubs[0].name, '업데이트된 클럽'); expect(localService.currentClub?.name, '업데이트된 클럽'); }); test('updateClub 메서드가 직접 클럽 객체가 반환되는 경우도 처리해야 함', () async { // given - 테스트 로컬 인스턴스 사용 final localMock = MockApiService(); final localService = ClubService.forTest(localMock); await localService.initialize(testToken); final updates = {'name': '업데이트된 클럽'}; final updatedClub = Map.from(testClub); updatedClub['name'] = '업데이트된 클럽'; when(localMock.post('${ApiConfig.clubs}/user')) .thenAnswer((_) async => [testClub]); await localService.fetchUserClubs(); when(localMock.post( ApiConfig.clubs, data: {'clubId': testClubId}, )).thenAnswer((_) async => testClub); await localService.fetchClubById(testClubId); // 준비 단계 상호작용 초기화로 검증 범위 격리 clearInteractions(localMock); // 직접 클럽 객체 반환 시뮬레이션 when(localMock.put( ApiConfig.clubs, data: { 'clubId': testClubId, ...updates, }, )).thenAnswer((_) async => updatedClub); // when final result = await localService.updateClub(testClubId, updates); // 상태 수렴 보장 await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); // 추가 폴링: 이름 반영 수렴까지 대기 (최대 2.5s) const total = Duration(milliseconds: 2500); const step = Duration(milliseconds: 25); var waited = Duration.zero; while ( (localService.clubs.isEmpty || localService.clubs.first.name != '업데이트된 클럽' || localService.currentClub?.name != '업데이트된 클럽') && waited < total ) { await Future.delayed(step); waited += step; } // idle 보장 및 소폭 지연 추가로 최종 수렴 보장 await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); // then - 상태 결과 검증 expect(result.name, '업데이트된 클럽'); expect(localService.clubs[0].name, '업데이트된 클럽'); expect(localService.currentClub?.name, '업데이트된 클럽'); }); }); }