数据层测试:Room数据库、DAO、迁移与存储

数据层是整个应用的基石。你想想看,如果存进去的数据读出来是错的,那UI层再漂亮也没用。今天我们就来聊聊Android数据层的测试,重点放在Room数据库、DAO、迁移,以及SharedPreferences、DataStore和文件存储上。

这部分内容,我建议你把它当作「必杀技」来掌握。因为在实际项目中,数据层出问题往往是最隐蔽的——它不像UI闪退那么明显,但后果可能更严重。

核心要点:数据层测试的目标是确保数据的读写、迁移、持久化都符合预期。说白了,就是保证数据「存得进去、读得出来、格式不变」。

Room数据库测试:@RunWith与@JUnit4

Room是Google官方推荐的本地数据库方案。测试Room数据库,我们通常用@RunWith(AndroidJUnit4::class)来跑。为什么?因为Room依赖Android框架的SQLite实现,不能在纯JVM上跑。

我个人习惯在测试中创建一个内存数据库,而不是用实体设备上的文件数据库。这样测试跑得快,而且每次都是干净的环境。

@RunWith(AndroidJUnit4::class)
class UserDatabaseTest {

    private lateinit var database: UserDatabase

    @Before
    fun setUp() {
        // 创建内存数据库,避免污染真实存储
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            UserDatabase::class.java
        ).build()
    }

    @After
    fun tearDown() {
        database.close()
    }

    @Test
    fun testDatabaseCreation() {
        assertNotNull(database)
        assertTrue(database.isOpen)
    }
}

小技巧:inMemoryDatabaseBuilder创建的数据库,在进程结束后自动销毁。我一般会在@After里手动close,确保资源释放。

DAO测试:验证CRUD操作

DAO(Data Access Object)是操作数据库的接口。测试DAO时,我通常会覆盖增删改查的每个场景。别嫌麻烦,我在项目中遇到过因为DAO查询条件写错,导致线上数据混乱的事故。

@Test
fun testInsertAndQueryUser() {
    val user = User(id = 1, name = "张三", age = 28)
    database.userDao().insertUser(user)

    val loadedUser = database.userDao().getUserById(1)
    assertEquals("张三", loadedUser.name)
    assertEquals(28, loadedUser.age)
}

@Test
fun testUpdateUser() {
    val user = User(id = 1, name = "张三", age = 28)
    database.userDao().insertUser(user)

    val updatedUser = user.copy(name = "李四")
    database.userDao().updateUser(updatedUser)

    val result = database.userDao().getUserById(1)
    assertEquals("李四", result.name)
}

@Test
fun testDeleteUser() {
    val user = User(id = 1, name = "张三", age = 28)
    database.userDao().insertUser(user)
    database.userDao().deleteUser(user)

    val result = database.userDao().getUserById(1)
    assertNull(result)
}

你可能会问:「为什么要测这么基础的操作?」嗯,我曾经在一个项目里,因为实体类的字段类型从Int改成了Long,但DAO的查询语句没同步更新,结果数据全读不出来。这种坑,踩过一次就记住了。

数据库迁移测试:版本升级的守护神

数据库迁移是Room测试里最容易翻车的地方。我见过太多团队在发版前才发现迁移脚本写错了,导致用户数据丢失。

测试迁移的核心思路是:先创建一个旧版本的数据库,插入一些数据,然后执行迁移,最后验证数据是否完整。

@Test
fun testMigrationFrom1To2() {
    // 创建版本1的数据库
    val oldDb = Room.databaseBuilder(
        ApplicationProvider.getApplicationContext(),
        UserDatabase::class.java,
        "test_user_db"
    ).build()

    // 插入旧版本数据
    oldDb.userDao().insertUser(User(id = 1, name = "测试用户", age = 25))

    // 关闭旧数据库,模拟升级
    oldDb.close()

    // 执行迁移到版本2
    val newDb = Room.databaseBuilder(
        ApplicationProvider.getApplicationContext(),
        UserDatabase::class.java,
        "test_user_db"
    ).addMigrations(MIGRATION_1_2).build()

    // 验证数据还在
    val user = newDb.userDao().getUserById(1)
    assertNotNull(user)
    assertEquals("测试用户", user?.name)

    newDb.close()
}

注意:迁移测试一定要覆盖边界情况。比如旧版本有1000条数据,迁移后是否全部保留?我曾经因为迁移脚本里少写了一个ALTER TABLE,导致用户的历史记录全部丢失。那次的教训太深刻了。

SharedPreferences与DataStore测试

SharedPreferences是老牌的数据存储方案,DataStore是它的替代者。测试它们的关键在于:验证写入和读取的一致性。

我个人更推荐DataStore,因为它基于Flow,天然支持异步和响应式。但SharedPreferences在遗留项目中仍然大量存在。

// SharedPreferences测试
@Test
fun testSharedPreferences() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val prefs = context.getSharedPreferences("test_prefs", Context.MODE_PRIVATE)

    prefs.edit().putString("key", "value").apply()
    val result = prefs.getString("key", null)
    assertEquals("value", result)
}

// DataStore测试
@Test
fun testDataStore() = runTest {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val dataStore = context.dataStore

    dataStore.edit { prefs ->
        prefs[PreferenceKeys.USER_NAME] = "测试用户"
    }

    val userName = dataStore.data.first()[PreferenceKeys.USER_NAME]
    assertEquals("测试用户", userName)
}

避坑指南:DataStore的测试需要依赖runTest协程测试库。我刚开始用的时候忘了加这个,结果测试一直超时。后来才发现是协程作用域的问题。

文件存储测试:读写与权限

文件存储测试相对简单,但容易忽略权限问题。在Android 10以上,外部存储的访问策略变了,测试时要注意。

@Test
fun testFileReadWrite() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val file = File(context.filesDir, "test.txt")

    // 写入
    file.writeText("Hello, World!")
    assertTrue(file.exists())

    // 读取
    val content = file.readText()
    assertEquals("Hello, World!", content)

    // 删除
    file.delete()
    assertFalse(file.exists())
}

这里有个细节:filesDir是应用内部存储,不需要额外权限。但如果用外部存储,记得在测试中模拟权限授予。

知识体系总览

下面这张图概括了数据层测试的核心内容。我把它画出来,方便你对照复习。

数据层测试知识体系 数据层测试 Room数据库测试 DAO测试 迁移测试 存储测试 @RunWith(AndroidJUnit4) CRUD操作验证 版本升级验证 SharedPreferences DataStore 内存数据库 增删改查覆盖 数据完整性 读写一致性 Flow异步 核心原则:存得进去、读得出来、格式不变

测试策略总结

测试类型 核心关注点 常用工具 常见陷阱
Room数据库 数据库创建、版本管理 AndroidJUnit4, inMemoryDatabase 忘记关闭数据库连接
DAO测试 CRUD操作的正确性 JUnit, Truth断言库 字段类型变更未同步
迁移测试 版本升级数据不丢失 addMigrations, 旧版本数据模拟 迁移脚本遗漏字段
SharedPreferences 读写一致性 Context.getSharedPreferences 多进程并发问题
DataStore 异步读写、Flow响应 runTest, PreferenceKeys 协程作用域未正确配置
文件存储 文件读写、权限管理 File API, Context.filesDir Android 10+ 权限变化

数据层测试看起来琐碎,但它是整个应用的「压舱石」。我见过太多项目因为数据库迁移没测好,导致用户数据丢失,最后不得不发紧急修复版本。这种经历,一次就够了。

记住:测试不是浪费时间,而是在为未来的自己「买保险」。你投入的每一分钟,都会在某个深夜救你一命。

公众号:蓝海资料掘金营,微信deep3321