Android 仪器测试在测试使用 RoomDatabase.withTransaction 的挂起函数时冻结

Android instrumented test freezes when it tests a suspend function that uses RoomDatabase.withTransaction

我正在尝试测试以下 LocalDataSource 函数,NameLocalData.methodThatFreezes 函数,但它冻结了。我该如何解决这个问题?或者我怎样才能用另一种方式测试它?

Class待测

class NameLocalData(private val roomDatabase: RoomDatabase) : NameLocalDataSource {

  override suspend fun methodThatFreezes(someParameter: Something): Something {
    roomDatabase.withTransaction {
      try {
        // calling room DAO methods here
      } catch(e: SQLiteConstraintException) {
        // ...
      }
      return something
    }
  }
}

测试class

@MediumTest
@RunWith(AndroidJUnit4::class)
class NameLocalDataTest {
  private lateinit var nameLocalData: NameLocalData

  // creates a Room database in memory
  @get:Rule
  var roomDatabaseRule = RoomDatabaseRule()

  @get:Rule
  var instantTaskExecutorRule = InstantTaskExecutorRule()

  @Before
  fun setup() = runBlockingTest {
     initializesSomeData()
     nameLocalData = NameLocalData(roomDatabaseRule.db)
  }

 @Test
 fun methodThatFreezes() = runBlockingTest {
    nameLocalData.methodThatFreezes // test freezes
 }

 // ... others NameLocalDataTest tests where those functions been tested does not use
 // roomDatabase.withTransaction { } 
}

Gradle的文件配置

espresso_version = '3.2.0'
kotlin_coroutines_version = '1.3.3'
room_version = '2.2.5'

test_arch_core_testing = '2.1.0'
test_ext_junit_version = '1.1.1'
test_roboletric = '4.3.1'
test_runner_version = '1.2.0'

androidTestImplementation "androidx.arch.core:core-testing:$test_arch_core_testing"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.ext:junit:$test_ext_junit_version"
androidTestImplementation "androidx.test:rules:$test_runner_version"
androidTestImplementation "androidx.test:runner:$test_runner_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version"

上次我为 Room 数据库编写测试时,我只是简单地使用 runBlock,它对我有用... 你能看一下 this sample 并检查它是否也适合你吗?

编辑: 哎呀!我错过了这部分......我试过这个(在同一个样本中):

  1. 我使用 @Transaction
  2. 在我的 DAO 中定义了一个虚拟函数
@Transaction
suspend fun quickInsert(book: Book) {
    save(book)
    delete(book)
}
  1. 我觉得这才是问题的关键。将 setTransactionExecutor 添加到您的数据库实例化。
appDatabase = Room.inMemoryDatabaseBuilder(
    InstrumentationRegistry.getInstrumentation().context,
    AppDatabase::class.java
).setTransactionExecutor(Executors.newSingleThreadExecutor())
    .build()
  1. 最后,测试成功了 runBlocking
@Test
fun dummyTest() = runBlocking {
    val dao = appDatabase.bookDao();
    val id = dummyBook.id

    dao.quickInsert(dummyBook)

    val book = dao.bookById(id).first()
    assertNull(book)
}

this question

我尝试了很多方法来完成这项工作,使用了 runBlockingTest、使用了 TestCoroutineScope、尝试了 runBlocking、使用了 allowMainThreadQueriessetTransactionExecutorsetQueryExecutor 在我的内存数据库中。

但是直到我在 Android Developers Medium 博客的 Threading models in Coroutines and Android SQLite API 文章中找到这个 comment thread 之前,没有任何效果,其他人提到了 运行。作者 Daniel Santiago 说:

I’m not sure what Robolectric might be doing under the hood that could cause withTransaction to never return. We usually don’t have Robolectric tests but we have plenty of Android Test examples if you want to try that route: https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt

我能够通过将测试从 Robolectric 测试更改为 Android 测试并使用 runBlocking

来修复我的测试

这是来自 google 来源的示例:

    @Before
    @Throws(Exception::class)
    fun setUp() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            TestDatabase::class.java
        )
            .build()
        booksDao = database.booksDao()
    }

    @Test
    fun runSuspendingTransaction() {
        runBlocking {
            database.withTransaction {
                booksDao.insertPublisherSuspend(
                    TestUtil.PUBLISHER.publisherId,
                    TestUtil.PUBLISHER.name
                )
                booksDao.insertBookSuspend(TestUtil.BOOK_1.copy(salesCnt = 0))
                booksDao.insertBookSuspend(TestUtil.BOOK_2)
                booksDao.deleteUnsoldBooks()
            }
            assertThat(booksDao.getBooksSuspend())
                .isEqualTo(listOf(TestUtil.BOOK_2))
        }
    }