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/member_service.dart'; import 'package:lanebow/services/api_service.dart'; import 'package:lanebow/services/event_bus.dart'; // EventBus().fire() 호출에 필요 import 'package:lanebow/config/api_config.dart'; import 'package:lanebow/utils/test_utils.dart'; // 모킹 클래스 생성 @GenerateMocks([ApiService]) import 'member_service_test.mocks.dart'; void main() { late MemberService memberService; late MockApiService mockApiService; final testToken = 'test_token'; final testClubId = 'test_club_id'; final testMemberId = 'test_member_id'; // 테스트 데이터 초기화 final testMember = { 'id': 'test_member_id', 'name': '테스트 회원', 'email': 'test@example.com', 'phone': '010-1234-5678', 'gender': '남성', 'birthYear': 1990, 'level': '중급', 'position': '회원', 'joinDate': DateTime.now().toIso8601String(), 'clubId': 'test_club_id', 'isActive': true, }; setUp(() { // 테스트 모드 강제 설정 TestUtils.setTestMode(true); // 테스트별 고유 ID 생성 및 설정 - 일관된 패턴 적용 final testId = 'member_service_test_${DateTime.now().millisecondsSinceEpoch}'; EventBus.setCurrentTestId(testId); // 이 호출로 모든 인스턴스가 초기화됨 // API 서비스 모킹 생성 (한 번만 생성) mockApiService = MockApiService(); // 토큰 설정 모킹 when(mockApiService.setToken(any)).thenReturn(null); // SharedPreferences 모킹 설정 SharedPreferences.setMockInitialValues({ ApiConfig.tokenKey: testToken, ApiConfig.clubIdKey: testClubId, }); // 상수 정의 - 모든 테스트에서 동일한 값 사용 const newClubId = 'new_club_id'; // 클럽 회원 목록 모킹 - 테스트 클럽 ID when(mockApiService.post( '${ApiConfig.clubs}/members', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testMember]); // 클럽 회원 목록 모킹 - new_club_id (명시적으로 설정) when(mockApiService.post( '${ApiConfig.clubs}/members', data: {'clubId': newClubId}, )).thenAnswer((_) async => [testMember]); // 회원 추가 모킹 when(mockApiService.post( '${ApiConfig.clubs}/members/add', data: anyNamed('data'), )).thenAnswer((_) async => {'id': testMemberId, ...testMember}); // 회원 업데이트 모킹 when(mockApiService.post( '${ApiConfig.clubs}/members/update', data: anyNamed('data'), )).thenAnswer((_) async => { 'id': testMemberId, 'name': '업데이트된 회원', 'email': 'updated@example.com', 'phone': '010-5555-5555', ...testMember, }); // 회원 삭제 모킹 when(mockApiService.post( '${ApiConfig.clubs}/members/delete', data: anyNamed('data'), )).thenAnswer((_) async => {'success': true}); // MemberService 생성 memberService = MemberService.forTest(mockApiService); // 초기화는 각 테스트에서 필요할 때 직접 호출 }); tearDown(() async { if (kDebugMode) { debugPrint('\n===== MemberService 테스트 tearDown 시작 ====='); debugPrint('시간: ${DateTime.now().toIso8601String()}'); debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}'); debugPrint('EventBus 테스트 인스턴스 수: ${EventBus.testInstanceCount}'); debugPrint('타이머 상태: ${TestUtils.isTimerPending() ? '활성' : '비활성'}'); } // 테스트 종료 후 정리 작업 try { // 서비스 정리 (비동기 dispose 호출) if (kDebugMode) { debugPrint('MemberService tearDown: dispose 호출 시작'); } await memberService.dispose(); if (kDebugMode) { debugPrint('MemberService tearDown: dispose 호출 완료'); } } catch (e) { // dispose 중 오류 무시 if (kDebugMode) { debugPrint('MemberService dispose 중 오류 무시: $e'); } } // 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); // EventBus 전체 인스턴스 초기화 후 현재 테스트 ID 해제 if (kDebugMode) { debugPrint('MemberService tearDown: EventBus.resetAllTestInstances 호출 시작'); } await EventBus.resetAllTestInstances(); if (kDebugMode) { debugPrint('MemberService tearDown: EventBus.resetAllTestInstances 호출 완료'); } if (kDebugMode) { debugPrint('MemberService tearDown: EventBus.clearCurrentTestId 호출 시작'); } await EventBus.clearCurrentTestId(); if (kDebugMode) { debugPrint('MemberService tearDown: EventBus.clearCurrentTestId 호출 완료'); } // 추가 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); await Future.microtask(() {}); // 테스트 모드 해제 TestUtils.setTestMode(false); if (kDebugMode) { debugPrint('\n===== MemberService 테스트 tearDown 완료 ====='); debugPrint('시간: ${DateTime.now().toIso8601String()}'); debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}'); debugPrint('EventBus 테스트 인스턴스 수: ${EventBus.testInstanceCount}'); debugPrint('타이머 상태: ${TestUtils.isTimerPending() ? '활성' : '비활성'}'); debugPrint('=======================================\n'); } // 추가 지연으로 비동기 작업 완료 대기 await Future.delayed(Duration(milliseconds: 100)); }); group('MemberService 테스트', () { test('initialize 메서드가 토큰을 설정하고 이벤트를 구독해야 함', () async { // given // 테스트용 새 클럽 ID - setUp에서 정의한 값과 동일하게 사용 const newClubId = 'new_club_id'; // 테스트 시작 전 초기화 확인 memberService = MemberService.forTest(mockApiService); // when memberService.initialize(testToken); // then verify(mockApiService.setToken(testToken)).called(1); // 이벤트 구독 테스트 // 새 클럽 ID로 이벤트 발생 EventBus().fire(ClubChangedEvent(newClubId)); // 비동기 처리를 위한 지연 추가 - 조금 더 길게 설정 await Future.delayed(Duration(milliseconds: 800)); // 새 클럽 ID로 API 호출 확인 verify(mockApiService.post( '/club/members', data: {'clubId': newClubId}, )).called(1); }); test('setClubId 메서드가 클럽 ID를 설정하고 회원 목록을 가져와야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 테스트용 클럽 ID const clubId = 'test_club_id'; // when await memberService.setClubId(clubId); // then verify(mockApiService.post( '${ApiConfig.clubs}/members', data: {'clubId': clubId}, )).called(1); // 회원 목록 확인 expect(memberService.members.length, 1); expect(memberService.members[0].id, testMember['id']); }); test('fetchClubMembers 메서드가 클럽의 회원 목록을 가져와야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 클럽 ID 설정 await memberService.setClubId(testClubId); // 초기화 후 모든 호출 기록 초기화 clearInteractions(mockApiService); // when await memberService.fetchClubMembers(); // then verify(mockApiService.post( '${ApiConfig.clubs}/members', data: {'clubId': testClubId}, )).called(1); }); test('addMember 메서드가 새 회원을 추가해야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 클럽 ID 설정 await memberService.setClubId(testClubId); // 초기화 후 모든 호출 기록 초기화 clearInteractions(mockApiService); final newMemberData = { 'name': '새 회원', 'email': 'new@example.com', 'phone': '010-9876-5432', 'clubId': testClubId, }; // when final result = await memberService.addMember(newMemberData); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/add', data: newMemberData, )).called(1); expect(result.id, testMember['id']); }); test('addMember 메서드가 직접 회원 객체가 반환되는 경우도 처리해야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 클럽 ID 설정 await memberService.setClubId(testClubId); // 초기화 후 모든 호출 기록 초기화 clearInteractions(mockApiService); final newMemberData = { 'name': '새 회원', 'email': 'new@example.com', 'phone': '010-9876-5432', 'clubId': testClubId, }; when(mockApiService.post( '${ApiConfig.clubs}/members/add', data: newMemberData, )).thenAnswer((_) async => testMember); // when final result = await memberService.addMember(newMemberData); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/add', data: newMemberData, )).called(1); expect(result.id, testMember['id']); }); test('updateMember 메서드가 회원 정보를 업데이트해야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 클럽 ID 설정 await memberService.setClubId(testClubId); clearInteractions(mockApiService); final updateMemberData = { 'name': '업데이트된 회원', 'email': 'updated@example.com', 'phone': '010-5555-5555', }; // 업데이트된 회원 데이터 mock 설정 when(mockApiService.post( '${ApiConfig.clubs}/members/update', data: anyNamed('data'), )).thenAnswer((_) async => {'member': { ...testMember, 'name': '업데이트된 회원', 'email': 'updated@example.com', 'phone': '010-5555-5555', }}); // when final result = await memberService.updateMember(testMemberId, updateMemberData); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/update', data: anyNamed('data'), )).called(1); expect(result.name, '업데이트된 회원'); expect(result.email, 'updated@example.com'); expect(result.phone, '010-5555-5555'); }); test('deleteMember 메서드가 회원을 삭제해야 함', () async { // given // 초기화 먼저 호출 memberService.initialize(testToken); // 클럽 ID 설정 await memberService.setClubId(testClubId); clearInteractions(mockApiService); // when final result = await memberService.deleteMember(testMemberId); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/delete', data: anyNamed('data'), )).called(1); expect(result, true); }); test('ClubChangedEvent가 발생하면 setClubId가 호출되어야 함', () async { // given // 테스트용 새 클럽 ID - setUp에서 정의한 값과 동일하게 사용 const newClubId = 'new_club_id'; // 테스트 시작 전 초기화 확인 memberService = MemberService.forTest(mockApiService); // 초기화 먼저 호출 memberService.initialize(testToken); // 이전 호출 기록 초기화 clearInteractions(mockApiService); try { // when // 이벤트 발생 EventBus().fire(ClubChangedEvent(newClubId)); // 비동기 처리를 위한 지연 추가 - 조금 더 길게 설정 await Future.delayed(Duration(milliseconds: 800)); // then // 새 클럽 ID로 API 호출 확인 verify(mockApiService.post( '/club/members', data: {'clubId': newClubId}, )).called(1); } finally { // 테스트 종료 시 memberService 정리 memberService.dispose(); // 추가 구독이 있을 경우를 대비해 EventBus 인스턴스 정리 await EventBus.resetAllTestInstances(); } }); // 이벤트 구독 테스트 추가 test('dispose 메서드가 이벤트 구독을 취소해야 함', () async { // 테스트 시작 전 모든 인스턴스 초기화 await EventBus.resetAllTestInstances(); // 테스트 ID 명시적 설정 final testId = 'member_service_dispose_test_${DateTime.now().millisecondsSinceEpoch}'; EventBus.setCurrentTestId(testId); if (kDebugMode) { debugPrint('\n===== dispose 테스트 시작 ====='); debugPrint('테스트 ID 설정됨: $testId'); debugPrint('초기 구독 수: ${EventBus.activeSubscriptionCount}'); } // given final memberService = MemberService.forTest(mockApiService); memberService.initialize(testToken); if (kDebugMode) { debugPrint('memberService 초기화 후 구독 수: ${EventBus.activeSubscriptionCount}'); } // 테스트를 위한 추가 구독이 필요하다면 여기서 추가 // final subscription = EventBus().on().listen((_) {}); // when - dispose 실행 if (kDebugMode) { debugPrint('memberService.dispose() 호출 전 구독 수: ${EventBus.activeSubscriptionCount}'); } // dispose 호출 및 비동기 작업 완료 대기 memberService.dispose(); // 비동기 작업 완료를 위한 충분한 대기 시간 확보 (300ms -> 500ms) await Future.delayed(Duration(milliseconds: 500)); // 모든 타이머 완료 확인을 위한 추가 대기 await Future.microtask(() {}); // 현재 대기 중인 모든 microtask 완료 보장 if (kDebugMode) { debugPrint('memberService.dispose() 호출 후 구독 수: ${EventBus.activeSubscriptionCount}'); debugPrint('===== dispose 테스트 종료 =====\n'); } // 구독 수가 0이어야 함 (모든 구독이 취소되었으므로) expect(EventBus.activeSubscriptionCount, equals(0), reason: '모든 구독이 취소되어 초기 상태로 돌아가야 함'); // 테스트 종료 전 모든 리소스 정리 확인 await EventBus.resetAllTestInstances(); await EventBus.clearCurrentTestId(); // 추가 대기 (타이머 생성 없이 microtask만) await Future.microtask(() {}); }); }); // 테스트 종료 후 전역 정리 tearDownAll(() async { if (kDebugMode) { debugPrint('\n===== MemberService 테스트 tearDownAll 시작 ====='); debugPrint('시간: ${DateTime.now().toIso8601String()}'); debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}'); debugPrint('EventBus 테스트 인스턴스 수: ${EventBus.testInstanceCount}'); debugPrint('타이머 상태: ${TestUtils.isTimerPending() ? '활성' : '비활성'}'); } // 모든 테스트 종료 후 리소스 정리 if (kDebugMode) { debugPrint('MemberService tearDownAll: EventBus.resetAllTestInstances 호출 시작'); } await EventBus.resetAllTestInstances(); if (kDebugMode) { debugPrint('MemberService tearDownAll: EventBus.resetAllTestInstances 호출 완료'); } if (kDebugMode) { debugPrint('MemberService tearDownAll: EventBus.clearCurrentTestId 호출 시작'); } await EventBus.clearCurrentTestId(); if (kDebugMode) { debugPrint('MemberService tearDownAll: EventBus.clearCurrentTestId 호출 완료'); } // 모든 비동기 작업 완료 확인 (타이머 생성 없이 microtask만) await Future.microtask(() {}); await Future.microtask(() {}); await Future.microtask(() {}); if (kDebugMode) { debugPrint('\n===== MemberService 테스트 tearDownAll 완료 ====='); debugPrint('시간: ${DateTime.now().toIso8601String()}'); debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}'); debugPrint('EventBus 테스트 인스턴스 수: ${EventBus.testInstanceCount}'); debugPrint('타이머 상태: ${TestUtils.isTimerPending() ? '활성' : '비활성'}'); debugPrint('=======================================\n'); } }); }