AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证
前两篇分别覆盖了模型单元测试和 Widget 组件测试。现在进入测试金字塔的中间层——数据层集成测试。E-Brufen 使用 Hive CE 作为本地存储引擎MoodStorage封装了完整的 CRUD 操作。本文将演示如何为一个基于 Hive 的 ChangeNotifier 编写可靠的集成测试。1. 测试数据层的挑战测试数据层与测试纯逻辑有很大不同挑战说明测试隔离每次测试需要独立的数据库实例互不干扰环境差异鸿蒙真机、模拟器、PC 上的文件系统不同异步操作Hive 的openBox、put、delete都是异步的副作用notifyListeners()产生副作用需验证但不阻塞E-Brufen 的解决方案是使用临时目录 setUp/tearDown 生命周期管理。2. MoodStorage 架构回顾在进入测试之前先回顾被测对象。MoodStorage是一个ChangeNotifier内部使用 Hive Box 存储 JSON 序列化的情绪记录classMoodStorageextendsChangeNotifier{staticconst_boxNamemoods;Box?_box;int _nextId1;Futurevoidinit()async{_boxawaitHive.openBox(_boxName);if(_box!.isNotEmpty){_nextId_box!.keys.foldint(0,(max,k)...)1;}}Futureintinsert(MoodEntryentry)async{...}ListMoodEntrygetAll(){...}ListMoodEntrygetByDate(DateTimedate){...}ListMoodEntrygetByWeek(DateTimeanyDay){...}Futurevoidupdate(int id,MoodEntryupdated)async{...}Futurevoiddelete(int id)async{...}Mapint,intgetWeeklyMoodCounts(DateTimeanyDay){...}}注意getAll、getByDate、getByWeek是同步方法——它们从内存中的_box直接读取。而insert、update、delete是异步的因为它们涉及 Hive 的磁盘写入。3. 测试基础设施setUp 和 tearDown每次测试都需要一个全新的、干净的 Hive 实例。我们利用setUp创建临时目录和初始化存储用tearDown清理importdart:io;importpackage:flutter_test/flutter_test.dart;importpackage:hive_ce/hive_ce.dart;importpackage:firstproject/data/mood_storage.dart;importpackage:firstproject/models/mood_entry.dart;voidmain(){lateMoodStoragestorage;setUp(()async{finaltmpDirDirectory.systemTemp.createTempSync(ebrufen_test_);Hive.init(tmpDir.path);storageMoodStorage();awaitstorage.init();});tearDown(()async{storage.dispose();});几个关键设计决策createTempSync(ebrufen_test_)每次测试创建一个独立的临时目录前缀ebrufen_test_便于识别和手动清理。Hive.init(tmpDir.path)必须显式调用因为测试环境没有Hive.initFlutter()所需的 Flutter 平台支持。storage.dispose()关闭 Hive Box释放文件句柄。如果忘了调用后续测试可能因为文件锁冲突而失败。4. 插入和查询测试这是最基础的数据流验证——写一条记录再读出来test(insert and retrieve a mood entry,()async{finalnowDateTime.now();finalidawaitstorage.insert(MoodEntry(moodType:MoodType.happy,note:今天很开心,createdAt:now,updatedAt:now,));expect(id,greaterThan(0));finalallstorage.getAll();expect(all.length,1);expect(all.first.moodType,MoodType.happy);expect(all.first.note,今天很开心);});验证顺序断言id 0——自增 ID 从 1 开始断言列表长度为 1断言字段内容与写入一致不必验证 ID 等于具体值如expect(id, 1)因为如果多个测试共享同一个 Hive 实例ID 会累积。使用greaterThan(0)更稳健。5. 日期过滤测试getByDate根据日期过滤记录——这是日记应用最常用的查询test(getByDate filters correctly,()async{finaltodayDateTime.now();finalyesterdaytoday.subtract(constDuration(days:1));awaitstorage.insert(MoodEntry(moodType:MoodType.happy,note:today,createdAt:today,updatedAt:today));awaitstorage.insert(MoodEntry(moodType:MoodType.sad,note:yesterday,createdAt:yesterday,updatedAt:yesterday));finaltodayMoodsstorage.getByDate(today);expect(todayMoods.length,1);expect(todayMoods.first.note,today);});这个测试验证了插入两条跨日期的记录后getByDate(today)只返回今天的 1 条note字段确实匹配的是 “today” 而不是 “yesterday”getByDate的内部实现使用DateTime(year, month, day)构造当天零点配合add(Duration(days: 1))构造次日零点实现范围查询。如果时区处理有误在跨时区环境中可能漏掉记录——这是集成测试的价值所在。6. 更新和删除测试更新和删除都需要验证修改是否真正持久化test(update mood entry,()async{finalnowDateTime.now();finalidawaitstorage.insert(MoodEntry(moodType:MoodType.tired,createdAt:now,updatedAt:now));awaitstorage.update(id,MoodEntry(moodType:MoodType.happy,createdAt:now,updatedAt:DateTime.now()));finalallstorage.getAll();expect(all.first.moodType,MoodType.happy);});test(delete mood entry,()async{finalnowDateTime.now();finalidawaitstorage.insert(MoodEntry(moodType:MoodType.angry,createdAt:now,updatedAt:now));awaitstorage.delete(id);finalallstorage.getAll();expect(all.length,0);});两个测试的共同模式先插入再更新/删除最后重新读取验证。这种先写后读的模式模拟了真实用户操作流程。7. 统计查询测试getWeeklyMoodCounts返回本周每种情绪的计数——这是情绪图表的数据源test(getWeeklyMoodCounts returns counts for a week,()async{finalmondayDateTime(2026,7,6);for(vari0;i5;i){finalmoodVals[MoodType.happy,MoodType.happy,MoodType.tired,MoodType.sad,MoodType.happy];awaitstorage.insert(MoodEntry(moodType:moodVals[i],createdAt:monday.add(Duration(days:i)),updatedAt:monday.add(Duration(days:i)),));}finalcountsstorage.getWeeklyMoodCounts(monday);expect(counts[5],3);// happy 3expect(counts[3],1);// tired 1expect(counts[2],1);// sad 1});这里使用固定日期DateTime(2026, 7, 6)而非DateTime.now()——因为后者在不同时间运行会产生不同结果测试就不稳定了。8. ChangeNotifier 通知验证MoodStorage继承自ChangeNotifier每次insert、update、delete后都会调用notifyListeners()。我们可以验证这一点test(notifyListeners is called on insert,()async{varnotifiedfalse;storage.addListener(()notifiedtrue);awaitstorage.insert(MoodEntry(moodType:MoodType.calm,createdAt:DateTime.now(),updatedAt:DateTime.now(),));expect(notified,true);});9. 测试数据库 vs 生产数据库在 E-Brufen 的测试中我们使用了与生产相同的 Hive CE 实现但通过Directory.systemTemp隔离了数据文件维度测试环境生产环境初始化Hive.init(tmpDir.path)Hive.initFlutter()with fallback数据目录systemTemp/ebrufen_test_xxxx应用文档目录Box 名称moods相同moods相同数据持久化随 tearDown 销毁持久化到应用生命周期这种实现相同、路径隔离的策略保证了测试的环境相关性——如果 Hive CE 在鸿蒙上有兼容性问题测试也能捕获。提示E-Brufen 的main.dart中生产环境 Hive 初始化有降级逻辑Hive.initFlutter()失败时回退到Hive.init(Directory.systemTemp.path)这个设计与测试使用 systemTemp 的路径一致。10. 运行与调试# 运行所有数据层测试fluttertesttest/data/database_test.dart# 运行并显示详细输出fluttertesttest/data/database_test.dart--reporterexpanded# 运行特定测试fluttertest--plain-nameinsert and retrievetest/data/database_test.dart总结E-Brufen 的database_test.dart包含 5 个测试用例插入查询、日期过滤、更新、删除、周统计。通过setUp/tearDown管理测试隔离使用临时目录避免污染真实调用 Hive CE API 保证了测试的参考价值。下一篇我们将转向错误处理看看 E-Brufen 如何在鸿蒙环境下实现优雅降级。上一篇Widget 测试实战验证情绪选择器与首页卡片下一篇Flutter 鸿蒙应用错误处理与优雅降级策略E-Brufen Dev, Flutter 鸿蒙开发者项目地址AtomGit - E‑Brufen

相关新闻