AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证

前两篇分别覆盖了模型单元测试和 Widget 组件测试。现在进入测试金字塔的中间层------数据层集成测试。E-Brufen 使用 Hive CE 作为本地存储引擎,MoodStorage 封装了完整的 CRUD 操作。本文将演示如何为一个基于 Hive 的 ChangeNotifier 编写可靠的集成测试。

1. 测试数据层的挑战

测试数据层与测试纯逻辑有很大不同:

挑战 说明
测试隔离 每次测试需要独立的数据库实例,互不干扰
环境差异 鸿蒙真机、模拟器、PC 上的文件系统不同
异步操作 Hive 的 openBoxputdelete 都是异步的
副作用 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) { ... }
}

注意:getAllgetByDategetByWeek 是同步方法------它们从内存中的 _box 直接读取。而 insertupdatedelete 是异步的,因为它们涉及 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, '今天很开心');
  });

验证顺序:

  1. 断言 id > 0------自增 ID 从 1 开始
  2. 断言列表长度为 1
  3. 断言字段内容与写入一致

不必验证 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');
  });

这个测试验证了:

  1. 插入两条跨日期的记录后,getByDate(today) 只返回今天的 1 条
  2. 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,每次 insertupdatedelete 后都会调用 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

相关推荐
进击的荆棘1 小时前
Linux——基础指令(中)
linux·服务器·linux基础指令
FrameNotWork1 小时前
HarmonyOS 6.0 WebView嵌入与交互
华为·交互·harmonyos
b130538100491 小时前
鸿蒙应用开发实战【41】— 登录页面LoginPage完整开发
服务器·华为·harmonyos·鸿蒙系统
zt1985q2 小时前
本地部署开源轻量级监控平台 Beszel 并实现外部访问
运维·服务器·网络协议·开源
2501_918582372 小时前
鸿蒙应用开发实战【31】— CardDao 卡号增删改查完整实现
华为·harmonyos·鸿蒙系统
全堆鸿蒙2 小时前
鸿蒙应用开发实战【36】— 数据查询优化:predicates 链式调用
数据库·harmonyos·鸿蒙系统
L-影2 小时前
Server-Sent Events (SSE):让服务器“主动开口”的实时推送技术
运维·服务器·sse
solo_992 小时前
Android window属性全解析
android
特立独行的猫a2 小时前
HarmonyOS鸿蒙PC三方库移植:Node.js 三方库鸿蒙 PC 移植指南
华为·node.js·node·harmonyos·三方库移植·鸿蒙pc