
前两篇分别覆盖了模型单元测试和 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 序列化的情绪记录:
dart
class MoodStorage extends ChangeNotifier {
static const _boxName = 'moods';
Box? _box;
int _nextId = 1;
Future<void> init() async {
_box = await Hive.openBox(_boxName);
if (_box!.isNotEmpty) {
_nextId = _box!.keys.fold<int>(0, (max, k) => ...) + 1;
}
}
Future<int> insert(MoodEntry entry) async { ... }
List<MoodEntry> getAll() { ... }
List<MoodEntry> getByDate(DateTime date) { ... }
List<MoodEntry> getByWeek(DateTime anyDay) { ... }
Future<void> update(int id, MoodEntry updated) async { ... }
Future<void> delete(int id) async { ... }
Map<int, int> getWeeklyMoodCounts(DateTime anyDay) { ... }
}
注意:getAll、getByDate、getByWeek 是同步方法------它们从内存中的 _box 直接读取。而 insert、update、delete 是异步的,因为它们涉及 Hive 的磁盘写入。
3. 测试基础设施:setUp 和 tearDown
每次测试都需要一个全新的、干净的 Hive 实例。我们利用 setUp 创建临时目录和初始化存储,用 tearDown 清理:
dart
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive_ce.dart';
import 'package:firstproject/data/mood_storage.dart';
import 'package:firstproject/models/mood_entry.dart';
void main() {
late MoodStorage storage;
setUp(() async {
final tmpDir = Directory.systemTemp.createTempSync('ebrufen_test_');
Hive.init(tmpDir.path);
storage = MoodStorage();
await storage.init();
});
tearDown(() async {
storage.dispose();
});
几个关键设计决策:
createTempSync('ebrufen_test_') :每次测试创建一个独立的临时目录,前缀 ebrufen_test_ 便于识别和手动清理。
Hive.init(tmpDir.path) :必须显式调用,因为测试环境没有 Hive.initFlutter() 所需的 Flutter 平台支持。
storage.dispose():关闭 Hive Box,释放文件句柄。如果忘了调用,后续测试可能因为文件锁冲突而失败。
4. 插入和查询测试
这是最基础的数据流验证------写一条记录,再读出来:
dart
test('insert and retrieve a mood entry', () async {
final now = DateTime.now();
final id = await storage.insert(MoodEntry(
moodType: MoodType.happy,
note: '今天很开心',
createdAt: now,
updatedAt: now,
));
expect(id, greaterThan(0));
final all = storage.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 根据日期过滤记录------这是日记应用最常用的查询:
dart
test('getByDate filters correctly', () async {
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
await storage.insert(MoodEntry(
moodType: MoodType.happy, note: 'today',
createdAt: today, updatedAt: today));
await storage.insert(MoodEntry(
moodType: MoodType.sad, note: 'yesterday',
createdAt: yesterday, updatedAt: yesterday));
final todayMoods = storage.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. 更新和删除测试
更新和删除都需要验证"修改是否真正持久化":
dart
test('update mood entry', () async {
final now = DateTime.now();
final id = await storage.insert(MoodEntry(
moodType: MoodType.tired, createdAt: now, updatedAt: now));
await storage.update(id, MoodEntry(
moodType: MoodType.happy, createdAt: now, updatedAt: DateTime.now()));
final all = storage.getAll();
expect(all.first.moodType, MoodType.happy);
});
test('delete mood entry', () async {
final now = DateTime.now();
final id = await storage.insert(MoodEntry(
moodType: MoodType.angry, createdAt: now, updatedAt: now));
await storage.delete(id);
final all = storage.getAll();
expect(all.length, 0);
});
两个测试的共同模式:先插入,再更新/删除,最后重新读取验证。这种"先写后读"的模式模拟了真实用户操作流程。
7. 统计查询测试
getWeeklyMoodCounts 返回本周每种情绪的计数------这是情绪图表的数据源:
dart
test('getWeeklyMoodCounts returns counts for a week', () async {
final monday = DateTime(2026, 7, 6);
for (var i = 0; i < 5; i++) {
final moodVals = [
MoodType.happy, MoodType.happy, MoodType.tired,
MoodType.sad, MoodType.happy
];
await storage.insert(MoodEntry(
moodType: moodVals[i],
createdAt: monday.add(Duration(days: i)),
updatedAt: monday.add(Duration(days: i)),
));
}
final counts = storage.getWeeklyMoodCounts(monday);
expect(counts[5], 3); // happy = 3
expect(counts[3], 1); // tired = 1
expect(counts[2], 1); // sad = 1
});
这里使用固定日期 DateTime(2026, 7, 6) 而非 DateTime.now()------因为后者在不同时间运行会产生不同结果,测试就不稳定了。
8. ChangeNotifier 通知验证
MoodStorage 继承自 ChangeNotifier,每次 insert、update、delete 后都会调用 notifyListeners()。我们可以验证这一点:
dart
test('notifyListeners is called on insert', () async {
var notified = false;
storage.addListener(() => notified = true);
await storage.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. 运行与调试
bash
# 运行所有数据层测试
flutter test test/data/database_test.dart
# 运行并显示详细输出
flutter test test/data/database_test.dart --reporter expanded
# 运行特定测试
flutter test --plain-name "insert and retrieve" test/data/database_test.dart
总结
E-Brufen 的 database_test.dart 包含 5 个测试用例:插入查询、日期过滤、更新、删除、周统计。通过 setUp/tearDown 管理测试隔离,使用临时目录避免污染,真实调用 Hive CE API 保证了测试的参考价值。下一篇我们将转向错误处理,看看 E-Brufen 如何在鸿蒙环境下实现优雅降级。
上一篇 :[Widget 测试实战:验证情绪选择器与首页卡片](#Widget 测试实战:验证情绪选择器与首页卡片)
下一篇 :[Flutter 鸿蒙应用错误处理与优雅降级策略](#Flutter 鸿蒙应用错误处理与优雅降级策略)
E-Brufen Dev , Flutter & 鸿蒙开发者,项目地址:AtomGit - E‑Brufen