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/score_service.dart'; import 'package:lanebow/services/api_service.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 'score_service_test.mocks.dart'; void main() { late ScoreService scoreService; late MockApiService mockApiService; final testToken = 'test_token'; final testClubId = 'test_club_id'; final testMemberId = 'test_member_id'; final testEventId = 'test_event_id'; final testScoreId = 'test_score_id'; // 테스트 점수 데이터 final testScore = { 'id': 'test_score_id', 'memberId': 'test_member_id', 'eventId': 'test_event_id', 'clubId': 'test_club_id', 'frames': [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], 'totalScore': 55, 'date': DateTime.now().toIso8601String(), 'notes': '테스트 점수', }; // 테스트 통계 데이터 final testStatistics = { 'average': 150.5, 'highScore': 200, 'lowScore': 100, 'gamesPlayed': 10, 'additionalStats': { 'strikes': 5, 'spares': 3, }, }; setUp(() async { // 테스트 모드 강제 설정 TestUtils.setTestMode(true); // 테스트별 고유 ID 생성 및 설정 final testId = 'score_service_test_${DateTime.now().millisecondsSinceEpoch}'; await EventBusTestUtils.setUp(testId); // SharedPreferences 모킹 SharedPreferences.setMockInitialValues({ ApiConfig.clubIdKey: testClubId, }); // API 서비스 모킹 mockApiService = MockApiService(); // ScoreService 초기화 (forTest 생성자 사용) scoreService = ScoreService.forTest(mockApiService, clubId: testClubId); // 토큰 설정 scoreService.initialize(testToken); }); tearDown(() async { // 테스트에서 생성된 ScoreService 정리 try { scoreService.dispose(); } catch (_) {} // EventBus 표준 정리 await EventBusTestUtils.tearDown(); // 테스트 모드 해제 TestUtils.setTestMode(false); }); group('ScoreService 테스트', () { test('initialize 메서드가 토큰을 설정해야 함', () { // given // setUp에서 이미 initialize 호출됨 // then verify(mockApiService.setToken(testToken)).called(1); }); test('setMemberId 메서드가 회원 ID를 설정해야 함', () { // when scoreService.setMemberId(testMemberId); // then expect(scoreService.testMemberId, testMemberId); }); test('setEventId 메서드가 이벤트 ID를 설정해야 함', () { // when scoreService.setEventId(testEventId); // then expect(scoreService.testEventId, testEventId); }); test('fetchClubScores 메서드가 클럽의 점수 목록을 가져와야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); // when await scoreService.fetchClubScores(); // then verify(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).called(1); expect(scoreService.scores.length, 1); expect(scoreService.scores[0].id, testScoreId); expect(scoreService.scores[0].totalScore, 55); expect(scoreService.lastLoadedAt, isNotNull); expect(scoreService.lastError, isNull); }); test('fetchClubScores 메서드가 객체 형태의 응답도 처리해야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => {'scores': [testScore]}); // when await scoreService.fetchClubScores(); // then verify(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).called(1); expect(scoreService.scores.length, 1); expect(scoreService.scores[0].id, testScoreId); }); test('fetchMemberScores 메서드가 회원의 점수 목록을 가져와야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/members/scores', data: {'memberId': testMemberId}, )).thenAnswer((_) async => {'scores': [testScore]}); // when final result = await scoreService.fetchMemberScores(testMemberId); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/scores', data: {'memberId': testMemberId}, )).called(1); expect(result.length, 1); expect(result[0].id, testScoreId); expect(result[0].memberId, testMemberId); }); test('fetchEventScores 메서드가 이벤트의 점수 목록을 가져와야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/events/scores', data: {'eventId': testEventId}, )).thenAnswer((_) async => {'scores': [testScore]}); // when final result = await scoreService.fetchEventScores(testEventId); // then verify(mockApiService.post( '${ApiConfig.clubs}/events/scores', data: {'eventId': testEventId}, )).called(1); expect(result.length, 1); expect(result[0].id, testScoreId); expect(result[0].eventId, testEventId); }); test('addScore 메서드가 새 점수를 추가해야 함', () async { // given final newScoreData = { 'memberId': testMemberId, 'eventId': testEventId, 'clubId': testClubId, 'frames': [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], 'totalScore': 55, }; when(mockApiService.post( ApiConfig.scores, data: newScoreData, )).thenAnswer((_) async => {'score': testScore}); // when final result = await scoreService.addScore(newScoreData); // then verify(mockApiService.post( ApiConfig.scores, data: newScoreData, )).called(1); expect(result.id, testScoreId); expect(scoreService.scores.length, 1); expect(scoreService.scores[0].id, testScoreId); }); test('updateScore 메서드가 점수 정보를 업데이트해야 함', () async { // given // 먼저 점수 목록 가져오기 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); await scoreService.fetchClubScores(); final updates = {'totalScore': 60}; final updatedScore = Map.from(testScore); updatedScore['totalScore'] = 60; when(mockApiService.put( '${ApiConfig.scores}/$testScoreId', data: updates, )).thenAnswer((_) async => {'score': updatedScore}); // when final result = await scoreService.updateScore(testScoreId, updates); // then verify(mockApiService.put( '${ApiConfig.scores}/$testScoreId', data: updates, )).called(1); expect(result.totalScore, 60); expect(scoreService.scores[0].totalScore, 60); }); test('deleteScore 메서드가 점수를 삭제해야 함', () async { // given // 먼저 점수 목록 가져오기 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); await scoreService.fetchClubScores(); when(mockApiService.delete( '${ApiConfig.scores}/$testScoreId', )).thenAnswer((_) async => {'success': true}); // when final result = await scoreService.deleteScore(testScoreId); // then verify(mockApiService.delete( '${ApiConfig.scores}/$testScoreId', )).called(1); expect(result, true); expect(scoreService.scores.length, 0); }); test('fetchClubScores: clubId가 null이면 예외를 던지고 isLoading=false 유지', () async { // given: prefs에서 clubId 제거 SharedPreferences.setMockInitialValues({}); // 새 서비스 인스턴스(클럽 미설정) final svc = ScoreService.forTest(mockApiService); svc.initialize(testToken); // when/then expect(() async => await svc.fetchClubScores(), throwsA(isA())); // 약간 대기 후 상태 확인(비동기 반영 안정화) await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); expect(svc.isLoading, isFalse); expect(svc.lastError, isNotNull); }); test('fetchClubScores: 빈 목록 응답이면 기존 scores를 비워야 함', () async { // given: 먼저 1개 로드 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); await scoreService.fetchClubScores(); expect(scoreService.scores.length, 1); // 빈 목록 응답으로 재설정 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => >[]); // when await scoreService.fetchClubScores(); // then expect(scoreService.scores, isEmpty); expect(scoreService.isLoading, isFalse); expect(scoreService.lastLoadedAt, isNotNull); expect(scoreService.lastError, isNull); }); test('addScore 실패 시 예외 전파, isLoading=false, scores 불변', () async { // given final payload = { 'memberId': testMemberId, 'eventId': testEventId, 'clubId': testClubId, 'frames': [1], 'totalScore': 1, }; when(mockApiService.post(ApiConfig.scores, data: payload)) .thenThrow(Exception('network')); final beforeLen = scoreService.scores.length; // when/then expect(() async => await scoreService.addScore(payload), throwsA(isA())); await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); expect(scoreService.isLoading, isFalse); expect(scoreService.scores.length, beforeLen); expect(scoreService.lastError, isNotNull); }); test('updateScore 실패 시 예외 전파, isLoading=false, scores 불변', () async { // given: 기존 점수 1개 로드 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); await scoreService.fetchClubScores(); final before = List.from(scoreService.scores); when(mockApiService.put('${ApiConfig.scores}/$testScoreId', data: anyNamed('data'))) .thenThrow(Exception('update failed')); // when/then expect(() async => await scoreService.updateScore(testScoreId, {'totalScore': 77}), throwsA(isA())); await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); expect(scoreService.isLoading, isFalse); expect(scoreService.scores.map((e) => e.totalScore), before.map((e) => e.totalScore)); expect(scoreService.lastError, isNotNull); }); test('deleteScore 실패 시 예외 전파, isLoading=false, scores 불변', () async { // given: 기존 점수 1개 로드 when(mockApiService.post( '${ApiConfig.clubs}/scores', data: {'clubId': testClubId}, )).thenAnswer((_) async => [testScore]); await scoreService.fetchClubScores(); final beforeLen = scoreService.scores.length; when(mockApiService.delete('${ApiConfig.scores}/$testScoreId')) .thenThrow(Exception('delete failed')); // when/then expect(() async => await scoreService.deleteScore(testScoreId), throwsA(isA())); await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); expect(scoreService.isLoading, isFalse); expect(scoreService.scores.length, beforeLen); }); test('fetchMemberScores: token 또는 clubId 없으면 예외', () async { // given: 새 서비스(초기화/클럽 미설정) final svc = ScoreService.forTest(mockApiService); // when/then expect(() async => await svc.fetchMemberScores(testMemberId), throwsA(isA())); }); test('fetchEventScores: token 또는 clubId 없으면 예외', () async { // given: 새 서비스(초기화/클럽 미설정) final svc = ScoreService.forTest(mockApiService); // when/then expect(() async => await svc.fetchEventScores(testEventId), throwsA(isA())); }); test('fetchMemberStatistics 메서드가 회원 통계를 가져와야 함', () async { // given when(mockApiService.post( '${ApiConfig.clubs}/members/stats', data: {'memberId': testMemberId}, )).thenAnswer((_) async => {'statistics': testStatistics}); // when final result = await scoreService.fetchMemberStatistics(testMemberId); // then verify(mockApiService.post( '${ApiConfig.clubs}/members/stats', data: {'memberId': testMemberId}, )).called(1); expect(result.average, 150.5); expect(result.highScore, 200); expect(result.lowScore, 100); expect(result.gamesPlayed, 10); expect(result.additionalStats?['strikes'], 5); expect(result.additionalStats?['spares'], 3); }); test('fetchClubStatistics 메서드가 클럽 통계를 가져와야 함', () async { // given final clubStats = { 'overall': testStatistics, 'monthly': testStatistics, }; when(mockApiService.post( '${ApiConfig.clubs}/scores/stats', data: {'clubId': testClubId}, )).thenAnswer((_) async => {'statistics': clubStats}); // when final result = await scoreService.fetchClubStatistics(); await EventBusTestUtils.waitForIdle(); await Future.delayed(const Duration(milliseconds: 50)); // then verify(mockApiService.post( '${ApiConfig.clubs}/scores/stats', data: {'clubId': testClubId}, )).called(1); expect(result.length, 2); expect(result['overall']?.average, 150.5); expect(result['monthly']?.highScore, 200); }); }); }