Files
nj/app/test/features/search/bloc/search_bloc_test.dart
iven 9c92cba87f test(app): ClassBloc + SearchBloc 单元测试 — 33 个测试全覆盖
ClassBloc (13 tests):
- 班级列表加载(成功/失败/loading 状态)
- 选中班级详情 + 子事件触发
- 成员/日记墙/主题/评语加载
- 创建班级 + 错误处理
- 加入班级 + 列表刷新
- 布置主题
- sharedToClass 过滤逻辑

SearchBloc (20 tests):
- 关键词搜索(标题/摘要/标签匹配、大小写不敏感)
- 心情筛选(null/有值/失败)
- 标签搜索
- 搜索历史(记录/去重)
- 清除搜索 + Tab 切换
- hasActiveFilter 属性
2026-06-03 19:03:29 +08:00

294 lines
9.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SearchBloc 单元测试
//
// 覆盖关键词搜索、标签搜索、心情筛选、搜索历史、清除搜索、tab 切换
// 使用 mocktail mock JournalRepository
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/repositories/journal_repository.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
import 'package:nuanji_app/features/search/bloc/search_bloc.dart';
// ===== Mock =====
class MockJournalRepository extends Mock implements JournalRepository {}
// ===== 测试数据工厂 =====
JournalEntry _makeJournal({
String id = 'j-1',
String title = '今天的心情日记',
Mood mood = Mood.happy,
String? contentExcerpt,
List<String> tags = const [],
}) {
return JournalEntry(
id: id,
authorId: 'user-1',
title: title,
date: DateTime(2026, 6, 1),
mood: mood,
contentExcerpt: contentExcerpt,
tags: tags,
createdAt: DateTime(2026, 6, 1),
updatedAt: DateTime(2026, 6, 1),
);
}
void main() {
late MockJournalRepository mockJournalRepo;
late SearchBloc bloc;
setUp(() {
mockJournalRepo = MockJournalRepository();
bloc = SearchBloc(journalRepository: mockJournalRepo);
});
tearDown(() {
bloc.close();
});
/// 辅助dispatch 并等待处理完成
Future<SearchState> dispatch(SearchEvent event) async {
bloc.add(event);
await Future<void>.delayed(const Duration(milliseconds: 50));
return bloc.state;
}
// ===== 关键词搜索 =====
group('SearchByKeyword', () {
test('空关键词不触发搜索,返回空结果', () async {
final state = await dispatch(const SearchByKeyword(''));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results, isEmpty);
});
test('纯空格关键词视为空', () async {
final state = await dispatch(const SearchByKeyword(' '));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results, isEmpty);
});
test('匹配标题中的关键词', () async {
final journals = [
_makeJournal(id: 'j1', title: '今天的心情日记'),
_makeJournal(id: 'j2', title: '周末旅行记'),
_makeJournal(id: 'j3', title: '读后感'),
];
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => journals);
final state = await dispatch(const SearchByKeyword('心情'));
expect(state, isA<SearchLoaded>());
final loaded = state as SearchLoaded;
expect(loaded.results.length, 1);
expect(loaded.results.first.id, 'j1');
expect(loaded.activeKeyword, '心情');
});
test('匹配内容摘要中的关键词', () async {
final journals = [
_makeJournal(id: 'j1', title: '日记', contentExcerpt: '今天心情很好,阳光明媚'),
_makeJournal(id: 'j2', title: '随笔', contentExcerpt: '天气阴沉'),
];
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => journals);
final state = await dispatch(const SearchByKeyword('阳光'));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results.length, 1);
expect((state as SearchLoaded).results.first.id, 'j1');
});
test('匹配标签中的关键词', () async {
final journals = [
_makeJournal(id: 'j1', tags: ['旅行', '周末']),
_makeJournal(id: 'j2', tags: ['学习']),
];
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => journals);
final state = await dispatch(const SearchByKeyword('旅行'));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results.length, 1);
});
test('大小写不敏感搜索', () async {
final journals = [
_makeJournal(id: 'j1', title: 'Happy Day'),
_makeJournal(id: 'j2', title: 'happy mood'),
];
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => journals);
final state = await dispatch(const SearchByKeyword('HAPPY'));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results.length, 2);
});
test('搜索失败返回 SearchError', () async {
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenThrow(Exception('网络错误'));
final state = await dispatch(const SearchByKeyword('测试'));
expect(state, isA<SearchError>());
});
test('搜索历史记录', () async {
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => []);
await dispatch(const SearchByKeyword('关键词A'));
await dispatch(const SearchByKeyword('关键词B'));
final state = await dispatch(const SearchByKeyword(''));
final loaded = state as SearchLoaded;
expect(loaded.searchHistory, ['关键词B', '关键词A']);
});
test('搜索历史去重', () async {
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => []);
await dispatch(const SearchByKeyword('重复'));
await dispatch(const SearchByKeyword('其他'));
await dispatch(const SearchByKeyword('重复'));
final state = await dispatch(const SearchByKeyword(''));
final loaded = state as SearchLoaded;
expect(loaded.searchHistory, ['重复', '其他']);
});
});
// ===== 心情筛选 =====
group('SearchByMood', () {
test('null mood 返回空结果', () async {
final state = await dispatch(const SearchByMood(null));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).results, isEmpty);
expect((state as SearchLoaded).activeMood, isNull);
});
test('按心情筛选日记', () async {
final journals = [
_makeJournal(id: 'j1', mood: Mood.happy),
_makeJournal(id: 'j2', mood: Mood.happy),
_makeJournal(id: 'j3', mood: Mood.sad),
];
when(() => mockJournalRepo.getJournals(mood: 'happy', page: 1, pageSize: 50))
.thenAnswer((_) async => journals.where((j) => j.mood == Mood.happy).toList());
final state = await dispatch(const SearchByMood(Mood.happy));
expect(state, isA<SearchLoaded>());
final loaded = state as SearchLoaded;
expect(loaded.results.length, 2);
expect(loaded.activeMood, 'happy');
});
test('心情筛选失败返回 SearchError', () async {
when(() => mockJournalRepo.getJournals(mood: 'happy', page: 1, pageSize: 50))
.thenThrow(Exception('网络错误'));
final state = await dispatch(const SearchByMood(Mood.happy));
expect(state, isA<SearchError>());
});
});
// ===== 标签搜索 =====
group('SearchByTag', () {
test('按标签筛选日记', () async {
final journals = [
_makeJournal(id: 'j1', tags: ['旅行', '周末']),
];
when(() => mockJournalRepo.getJournals(tag: '旅行', page: 1, pageSize: 50))
.thenAnswer((_) async => journals);
final state = await dispatch(const SearchByTag('旅行'));
expect(state, isA<SearchLoaded>());
final loaded = state as SearchLoaded;
expect(loaded.results.length, 1);
expect(loaded.activeTag, '旅行');
expect(loaded.searchHistory, contains('旅行'));
});
test('标签搜索失败返回 SearchError', () async {
when(() => mockJournalRepo.getJournals(tag: '不存在', page: 1, pageSize: 50))
.thenThrow(Exception('网络错误'));
final state = await dispatch(const SearchByTag('不存在'));
expect(state, isA<SearchError>());
});
});
// ===== 清除搜索 =====
group('SearchClear', () {
test('清除搜索返回空结果', () async {
// 先执行一次搜索
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => [_makeJournal()]);
await dispatch(const SearchByKeyword('测试'));
// 清除
final state = await dispatch(const SearchClear());
expect(state, isA<SearchLoaded>());
final loaded = state as SearchLoaded;
expect(loaded.results, isEmpty);
expect(loaded.activeKeyword, isNull);
expect(loaded.activeMood, isNull);
expect(loaded.activeTag, isNull);
});
});
// ===== Tab 切换 =====
group('SearchTabChanged', () {
test('切换 tab 更新 activeTab', () async {
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => []);
await dispatch(const SearchByKeyword('测试'));
final state = await dispatch(const SearchTabChanged(SearchResultTab.journal));
expect(state, isA<SearchLoaded>());
expect((state as SearchLoaded).activeTab, SearchResultTab.journal);
});
test('非 SearchLoaded 状态下切换 tab 无效', () async {
// 初始状态 SearchInitial不响应 tab 切换
final state = await dispatch(const SearchTabChanged(SearchResultTab.tag));
expect(state, isA<SearchInitial>());
});
});
// ===== hasActiveFilter =====
group('SearchLoaded.hasActiveFilter', () {
test('无筛选条件时 hasActiveFilter 为 false', () async {
final state = await dispatch(const SearchClear());
expect((state as SearchLoaded).hasActiveFilter, isFalse);
});
test('有关键词时 hasActiveFilter 为 true', () async {
when(() => mockJournalRepo.getJournals(page: 1, pageSize: 200))
.thenAnswer((_) async => []);
final state = await dispatch(const SearchByKeyword('测试'));
expect((state as SearchLoaded).hasActiveFilter, isTrue);
});
});
}