// MoodBloc 单元测试 // // 覆盖:心情统计加载、周期切换、API 解析、错误处理 // 使用 mocktail mock ApiClient import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:nuanji_app/data/models/journal_entry.dart'; import 'package:nuanji_app/data/remote/api_client.dart'; import 'package:nuanji_app/features/mood/bloc/mood_bloc.dart'; // ===== Mock ===== class MockApiClient extends Mock implements ApiClient {} // ===== 测试数据 ===== /// 模拟 API 返回的心情统计数据 Map _mockResponseBody({ List>? moodCounts, int streakDays = 5, int totalJournals = 12, String? dominantMood = 'happy', }) { return { 'data': { 'mood_counts': moodCounts ?? [ {'mood': 'happy', 'count': 8, 'percentage': 66.7}, {'mood': 'calm', 'count': 3, 'percentage': 25.0}, {'mood': 'sad', 'count': 1, 'percentage': 8.3}, ], 'streak_days': streakDays, 'total_journals': totalJournals, 'dominant_mood': dominantMood, }, }; } void main() { late MockApiClient mockApi; late MoodBloc bloc; setUpAll(() { registerFallbackValue(Response(requestOptions: RequestOptions())); }); setUp(() { mockApi = MockApiClient(); bloc = MoodBloc(api: mockApi); }); tearDown(() { bloc.dispose(); }); // ===== 辅助 ===== /// 配置 mock API.get 返回指定响应体 void _stubGet(Map body) { when(() => mockApi.get(any(), queryParams: any(named: 'queryParams'))) .thenAnswer((_) async => Response( requestOptions: RequestOptions(path: ''), data: body, )); } /// 配置 mock API.get 抛出异常 void _stubGetError(Object error) { when(() => mockApi.get(any(), queryParams: any(named: 'queryParams'))) .thenThrow(error); } /// 等待异步 _loadStats 完成 Future _waitForAsync() => Future.delayed(const Duration(milliseconds: 50)); // ===== 1. load() 设置 isLoading = true ===== test('load() 立即设置 isLoading = true', () { // 让 API 永不返回,这样 isLoading 会一直保持 true when(() => mockApi.get(any(), queryParams: any(named: 'queryParams'))) .thenAnswer((_) async { await Future.delayed(const Duration(seconds: 10)); return Response(requestOptions: RequestOptions(path: ''), data: {}); }); bloc.load(); expect(bloc.state.isLoading, isTrue); }); // ===== 2. load() 成功 → isLoading = false, stats 有数据 ===== test('load() 成功后 isLoading = false 且 stats 包含数据', () async { _stubGet(_mockResponseBody()); bloc.load(); await _waitForAsync(); expect(bloc.state.isLoading, isFalse); expect(bloc.state.errorMessage, isNull); expect(bloc.state.stats.streakDays, 5); expect(bloc.state.stats.totalJournals, 12); }); // ===== 3. load() 成功 → moodCounts 解析正确 ===== test('load() 成功后 moodCounts 解析正确', () async { _stubGet(_mockResponseBody()); bloc.load(); await _waitForAsync(); final counts = bloc.state.stats.moodCounts; expect(counts.length, 3); expect(counts[0].mood, Mood.happy); expect(counts[0].count, 8); expect(counts[0].percentage, 66.7); expect(counts[1].mood, Mood.calm); expect(counts[1].count, 3); expect(counts[1].percentage, 25.0); expect(counts[2].mood, Mood.sad); expect(counts[2].count, 1); expect(counts[2].percentage, 8.3); }); // ===== 4. load() 成功 → dominantMood 解析正确 ===== test('load() 成功后 dominantMood 解析正确', () async { _stubGet(_mockResponseBody(dominantMood: 'calm')); bloc.load(); await _waitForAsync(); expect(bloc.state.stats.dominantMood, Mood.calm); }); test('load() dominantMood 为 null 时不崩溃', () async { _stubGet(_mockResponseBody(dominantMood: null)); bloc.load(); await _waitForAsync(); expect(bloc.state.stats.dominantMood, isNull); expect(bloc.state.errorMessage, isNull); }); // ===== 5. load() 失败 → errorMessage = '加载统计数据失败' ===== test('load() API 失败时设置 errorMessage', () async { _stubGetError(Exception('网络错误')); bloc.load(); await _waitForAsync(); expect(bloc.state.isLoading, isFalse); expect(bloc.state.errorMessage, '加载统计数据失败'); }); // ===== 6. changePeriod 更新周期并触发加载 ===== test('changePeriod 更新 selectedPeriod 并触发重新加载', () async { _stubGet(_mockResponseBody(streakDays: 10)); bloc.changePeriod(StatsPeriod.month); await _waitForAsync(); expect(bloc.state.selectedPeriod, StatsPeriod.month); expect(bloc.state.isLoading, isFalse); expect(bloc.state.stats.streakDays, 10); }); // ===== 7. changePeriod 相同周期 → 不改变状态(no-op)===== test('changePeriod 相同周期时不改变状态', () async { // 先完成一次正常加载 _stubGet(_mockResponseBody(streakDays: 3)); bloc.load(); await _waitForAsync(); final stateBefore = bloc.state; expect(stateBefore.selectedPeriod, StatsPeriod.week); // 再次调用 changePeriod(week) — 应该是 no-op bloc.changePeriod(StatsPeriod.week); // 不等待异步,因为不应有新请求发出 expect(bloc.state.selectedPeriod, StatsPeriod.week); // state 应该完全不变(同一个对象引用) expect(identical(bloc.state, stateBefore), isTrue); }); }