Android(Kotlin)中Realm保存成功但测试断言失败问题排查
问题描述
在Android应用中使用Kotlin协程测试Realm数据库,实现了包含isEmpty()方法的ConnectionsRepository类,编写单元测试断言保存Connection对象前后的行为。尽管日志显示保存成功,但测试仍抛出AssertionError失败。
方法代码
// ConnectionsRepository override fun isEmpty(): Boolean { return RealmManager.getDefaultInstance().use { realm -> realm.where(Connection::class.java).count() == 0L } } suspend fun Connection.save(): Connection? = ConnectionsRepository.saveModel(this) override suspend fun saveModel(connection: Connection): Connection? { if (connection.createdAt == 0L) connection.createdAt = DateTime.now().withZone(DateTimeZone.UTC).millis connection.updatedAt = DateTime.now().withZone(DateTimeZone.UTC).millis return withContext(Dispatchers.IO) { var savedConnection: Connection? = null RealmManager.getDefaultInstance().use { realmDb -> realmDb.executeTransaction { transaction -> Log.d("RealmTransaction", "Saving connection: $connection") savedConnection = realmDb.copyFromRealm(transaction.copyToRealmOrUpdate(connection)) } } if (savedConnection == null) { Log.e("Error", "Connection was saved with error") } else { Log.e("Error", "Connection was saved successfully: $savedConnection") } savedConnection } }
测试代码
@RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class ConnectionsRepositoryTest : DatabaseTestCase() { @Test @Throws(Exception::class) fun isEmptyTest() = runBlockingTest { Assert.assertTrue(ConnectionsRepository.isEmpty()) runBlocking { Connection().setGuid("guid1").setId("1").setAccessToken("token4").save() advanceUntilIdle() // Wait for coroutine completion } Assert.assertFalse(ConnectionsRepository.isEmpty()) } }
日志信息
04-04 16:55:47.713 22806 22836 D RealmTransaction: Saving connection: Connection(...) 04-04 16:55:47.716 22806 22836 E Error: Connection was saved successfully: Connection(...)
已做排查
- 日志确认
Connection对象保存成功 - 使用
advanceUntilIdle()等待协程完全执行完毕
核心疑问
为何测试仍出现AssertionError失败?是否是Realm缓存或同步问题?
问题分析与解决方案
1. 嵌套runBlocking导致协程调度异常
测试代码中在runBlockingTest内部嵌套runBlocking,会破坏TestDispatcher的调度逻辑:runBlockingTest使用测试专用协程调度器,而嵌套的runBlocking会切换到默认调度器,导致advanceUntilIdle()无法正确等待保存操作完成。
修复: 移除嵌套的runBlocking,直接在runBlockingTest作用域内调用save():
@Test @Throws(Exception::class) fun isEmptyTest() = runBlockingTest { Assert.assertTrue(ConnectionsRepository.isEmpty()) Connection().setGuid("guid1").setId("1").setAccessToken("token4").save() advanceUntilIdle() Assert.assertFalse(ConnectionsRepository.isEmpty()) }
2. Realm线程绑定与实例可见性问题
Realm实例是线程绑定的,若RealmManager.getDefaultInstance()返回的实例配置不一致(比如意外创建多个内存数据库实例),会导致主线程的isEmpty()看不到IO线程保存的数据。
排查与修复:
- 检查
RealmManager.getDefaultInstance()实现,确保测试环境下所有操作使用同一个Realm配置(比如内存数据库)。 - 在
isEmpty()中添加日志打印实际计数,确认数据是否真的未写入:override fun isEmpty(): Boolean { return RealmManager.getDefaultInstance().use { realm -> val count = realm.where(Connection::class.java).count() Log.d("RealmDebug", "Current connection count: $count") count == 0L } }
3. 协程Dispatcher硬编码问题
saveModel中直接使用Dispatchers.IO,导致测试时无法替换为测试调度器,增加协程调度不确定性。
改进: 通过构造函数注入Dispatcher,便于测试替换:
class ConnectionsRepository( private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { override suspend fun saveModel(connection: Connection): Connection? { // ... return withContext(dispatcher) { // ... } } }
测试时注入TestCoroutineDispatcher:
class ConnectionsRepositoryTest : DatabaseTestCase() { private val testDispatcher = TestCoroutineDispatcher() private lateinit var repository: ConnectionsRepository @Before fun setup() { repository = ConnectionsRepository(testDispatcher) } @After fun teardown() { testDispatcher.cleanupTestCoroutines() } }
4. 测试隔离问题
若DatabaseTestCase未在每次测试前清理Realm数据库,前一次测试的数据残留会影响断言结果。
改进: 在@Before方法中清理数据库:
@Before fun setup() { RealmManager.getDefaultInstance().use { realm -> realm.executeTransaction { it.deleteAll() } } }
内容的提问来源于stack exchange,提问作者Morozov

