3. 数据层夹具设计

3.1 边界定义

数据层夹具(Data Layer Fixture)的边界位于业务逻辑层与真实数据库之间。它隔离了对实际 SQLite 实例、网络延迟、文件 I/O 的依赖,使单元测试或组件级测试仅关注数据访问契约的正确性。该边界确保:

  • 测试不依赖设备上的数据库文件路径或 schema 迁移历史。
  • DAO 方法返回的 LiveData、Flow 或 suspend 结果可以被精确控制。
  • Entity 的 id 生成、时间戳、关系映射不会因真实数据库自增而产生意外冲突。

3.2 分层架构

数据层夹具由三个逻辑分层构成,各层职责明确:

层名 组件 核心职责
Entity 模拟层 Entity Fixture 提供预构造的 Entity 对象(包括主键、外键、嵌套对象),模拟数据库真实行。
DAO 模拟层 DAO Fixture 或 Mock 实现 DAO 接口,返回预定数据或模拟异步操作;验证调用参数与次数。
初始化策略层 Database Fixture Builder 决定使用内存数据库、预填充 SQLite 文件还是纯 Mock,并管理生命周期。

3.3 组件职责

  • Entity Fixture:应覆盖 Entity 的所有字段,包括可空字段、默认值、enum 映射以及嵌入式类型(@Embedded)。通常使用构建器模式或 Kotlin 默认参数创建,避免每次测试重复编写数据。
  • DAO Fixture:若使用 Mock(如 MockK / Mockito),只需 stub 方法;若使用 Fake 实现,则维护一个线程安全的内存数据容器(如 ConcurrentHashMap),模拟增删改查逻辑。后者更贴近真实 DAO 行为,但需额外实现事务边界(通常用同步块简化)。
  • 初始化策略:三种主流方案——纯 Mock(速度最快,但丢失 Room 行为)、内存数据库(调用 Room.inMemoryDatabaseBuilder,接近真实环境)、预置文件(对 schema 版本和迁移要求严格)。

3.4 数据流与控制流

以内存数据库策略为例,典型控制流如下:

  1. 测试框架创建 Room.inMemoryDatabaseBuilder,注入 DAO 实例。
  2. 测试内使用 Entity Fixture 构造数据,通过 DAO 的 insert 方法写入。
  3. 被测业务逻辑调用 DAO 的查询方法,返回 Flow/LiveData。
  4. 验证层观察返回数据并与预期 Entity 比对。

信任链路要求:内存数据库的行为必须与真实 SQLite 数据库在 SQL 语法、主键冲突、事务回滚等方面完全一致。实际 room 内存数据库底层仍使用 SQLite,因此信任度较高,但在 Fts4、AutoMigration、PrePackagedDatabase 等特性上存在差异,需在验收时标注。

3.5 选型权衡

策略 速度 真实度 维护成本 风险点
纯 Mock DAO ★★★★★ ★★☆☆☆ 低 缺少 SQL 行为验证,隐藏 Regex/Constraint 错误
Room 内存数据库 ★★★★☆ ★★★★★ 中 依赖 databaseBuilder 实例;多表关联时索引重建慢
预填充 SQLite 文件 ★★★☆☆ ★★★★★ 高 版本升级时需重新生成文件;文件体积大
推荐组合:关键业务路径(如账单计算、消息排序)使用内存数据库;简单 CRUD 或纯展示层测试使用纯 Mock DAO,以平衡速度与可靠性。

3.6 风险点

  • 数据一致性问题:内存数据库在测试过程中可能因并发访问(如协程同时写入)导致脏数据,需在 test 类中添加 @RunWith(AndroidJUnit4::class) 并在每个测试后调用 database.clearAllTables()。
  • DTO 与 Entity 之间的差量:如果业务层直接暴露 DTO,Entity Fixture 可能与 DTO 的转换逻辑不匹配,造成假阳性。必须为转换函数编写独立的 fixture 验证。
  • DAO 签名变更:当 DAO 方法参数或返回值类型变化时,Mock 的 stub 语句若未同步更新会导致未预期的异常。建议使用 <pre><code>@VisibleForTesting</code> 标注并在 CI 中加入 Strict 模式。

3.7 落地步骤

  1. 定义 Entity Fixture 工厂:在 sharedTest 或 test 目录下创建 UserEntityFixture.kt,提供默认值。
  2. 创建 DAO 接口的 Fake 实现(可选)或 Mock 封装类。
  3. 编写数据库初始化基类:BaseDatabaseTest 包含 database 和 dao 的 lazy 属性,使用 @Before 初始化,@After 释放。
  4. 添加数据清理钩子:在每个测试方法前通过 database.clearAllTables() 重置状态。
  5. 编写验收测试:覆盖主要 CRUD 场景和边界条件(如插入重复主键、查询不存在记录、嵌套对象级联)。

3.8 验收标准

  • 所有数据层测试运行在 JVM 环境(不使用模拟器),单类测试执行时间 < 200ms。
  • 内存数据库测试覆盖 100% 的 DAO 方法(包括 suspend、Flow、LiveData 变体)。
  • 每组测试之间数据完全隔离,通过 clearAllTables() 确保无侧效应。
  • 实体 fixture 字段的默认值与数据库 schema 定义一致(可通过编译期反射或 Room 的 EntityProcessor 验证)。
数据层夹具分层设计 测试层 夹具层 数据源层 Test Case Entity Fixture DAO Mock / Fake In-Memory DB Real SQLite 虚线表示可选路径 — 实际测试通常只选一条路径(Mock 或 内存 DB 或 真实 DB)
建议:在团队新建 Room 项目时,优先实现内存数据库夹具策略。它能在不引入 Android 依赖(依赖 android.jar)的情况下运行大部分 DAO 测试,仅在需要测试 SQLite 原生功能(如 FTS、窗口函数)时才切换为预填充文件方案。