第3章:数据层夹具设计——Entity与DAO模拟、数据库初始化策略
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 数据流与控制流
以内存数据库策略为例,典型控制流如下:
- 测试框架创建
Room.inMemoryDatabaseBuilder,注入 DAO 实例。 - 测试内使用 Entity Fixture 构造数据,通过 DAO 的 insert 方法写入。
- 被测业务逻辑调用 DAO 的查询方法,返回 Flow/LiveData。
- 验证层观察返回数据并与预期 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 落地步骤
- 定义 Entity Fixture 工厂:在
sharedTest或test目录下创建UserEntityFixture.kt,提供默认值。 - 创建 DAO 接口的 Fake 实现(可选)或 Mock 封装类。
- 编写数据库初始化基类:
BaseDatabaseTest包含database和dao的 lazy 属性,使用@Before初始化,@After释放。 - 添加数据清理钩子:在每个测试方法前通过
database.clearAllTables()重置状态。 - 编写验收测试:覆盖主要 CRUD 场景和边界条件(如插入重复主键、查询不存在记录、嵌套对象级联)。
3.8 验收标准
- 所有数据层测试运行在 JVM 环境(不使用模拟器),单类测试执行时间 < 200ms。
- 内存数据库测试覆盖 100% 的 DAO 方法(包括 suspend、Flow、LiveData 变体)。
- 每组测试之间数据完全隔离,通过
clearAllTables()确保无侧效应。 - 实体 fixture 字段的默认值与数据库 schema 定义一致(可通过编译期反射或 Room 的 EntityProcessor 验证)。
建议:在团队新建 Room 项目时,优先实现内存数据库夹具策略。它能在不引入 Android 依赖(依赖 android.jar)的情况下运行大部分 DAO 测试,仅在需要测试 SQLite 原生功能(如 FTS、窗口函数)时才切换为预填充文件方案。