【发布时间】:2020-10-04 08:27:57
【问题描述】:
问题
预期
使用 JUnit 5 本地单元测试,在 TestCoroutineDispatcher() 中运行 Room 数据库 @Insert 和 Query。
观察到
Room 数据库@Insert 和@Query 在TestCoroutineDispatcher().runBlockingTest 内执行,导致出现以下错误。如果使用非测试调度程序 Dispatchers.IO 显式定义线程,则数据库调用将起作用。
错误日志:
无法访问主线程上的数据库,因为它可能会长时间锁定 UI。
实施
1.添加库
- JUnit 5:Setup
- Robolectric:Getting Started
- AndroidX:为 AndroidX 测试设置项目 > List of AndroidX Test dependencies
- Kotlin 协程:Using in your project
build.gradle (SomeProjectName)
dependencies {
...
// JUnit 5
classpath("de.mannodermaus.gradle.plugins:android-junit5:X.X.X")
}
build.gradle (:someModuleName)
apply plugin: "de.mannodermaus.android-junit5"
// JUnit 5
testImplementation "org.junit.jupiter:junit-jupiter-api:X.X.X"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:X.X.X"
// Robolectric
testImplementation "org.robolectric:robolectric:X.X.X"
testImplementation "androidx.test.ext:junit:X.X.X"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:X.X.X"
2。创建测试
一个。设置测试 Dispatcher 和 LiveData 执行器。
b.创建测试数据库:Test and debug your database。
c。确保测试数据库在与单元测试相同的 Dispatcher 上执行:Testing AndroidX Room + Kotlin Coroutines - @Eyal Guthmann
d。在TestCoroutineDispatcher().runBlockingTest 中运行数据库@Insert 和@Query。
SomeTest.kt
import androidx.test.core.app.ApplicationProvider
@ExperimentalCoroutinesApi
@Config(maxSdk = Build.VERSION_CODES.P, minSdk = Build.VERSION_CODES.P)
@RunWith(RobolectricTestRunner::class)
class SomeTest {
private val testDispatcher = TestCoroutineDispatcher()
@Test
fun someTest() = testDispatcher.runBlockingTest {
// Test setup, moved to test extension in production. Also, cleanup methods not included here for simplicity.
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
// Set LiveData Executor.
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
val appContext = ApplicationProvider.getApplicationContext<Context>()
// Room database setup
db = Room.inMemoryDatabaseBuilder(appContext, SomeDatabase::class.java)
.setTransactionExecutor(testDispatcher.asExecutor())
.setQueryExecutor(testDispatcher.asExecutor())
.build()
dao = db.someDao()
// Insert into database.
dao.insertData(mockDataList)
// Query database.
val someQuery = dao.queryData().toLiveData(PAGE_SIZE).asFlow()
someQuery.collect {
// TODO: Test something here.
}
// TODO: Make test assertions.
...
}
SomeDao.kt
@Dao
interface SomeDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertData(data: List<SomeData>)
@Query("SELECT * FROM someDataTable")
fun queryData(): DataSource.Factory<Int, SomeData>
}
尝试的解决方案
1。将suspend 修饰符添加到SomeDao.kt 的queryData 函数。
添加suspend后,随后调用queryData的方法必须要么实现suspend,要么使用launch从协程启动,如下所示。
这会导致编译器出错。
错误:不确定如何将 Cursor 转换为此方法的返回类型 (androidx.paging.DataSource.Factory)。
SomeDao.kt
@Dao
interface SomeDao {
...
@Query("SELECT * FROM someDataTable")
suspend fun queryData(): DataSource.Factory<Int, SomeData>
}
SomeRepo.kt
suspend fun getInitialData(pagedListBoundaryCallback: PagedList.BoundaryCallback<SomeData>) = flow {
emit(Resource.loading(null))
try {
dao.insertData(getDataRequest(...))
someDataQuery(pagedListBoundaryCallback).collect {
emit(Resource.success(it))
}
} catch (error: Exception) {
someDataQuery(pagedListBoundaryCallback).collect {
emit(Resource.error(error.localizedMessage!!, it))
}
}
}
SomeViewModel.kt
private suspend fun loadNetwork(toRetry: Boolean) {
repository.getInitialData(pagedListBoundaryCallback(toRetry)).onEach {
when (it.status) {
LOADING -> _viewState.value = ...
SUCCESS -> _viewState.value = ...
ERROR -> _viewState.value = ...
}
}.flowOn(coroutineDispatcherProvider.io()).launchIn(coroutineScope)
}
fun bindIntents(view: FeedView) {
view.loadNetworkIntent().onEach {
coroutineScope.launch(coroutineDispatcherProvider.io()) {
loadNetwork(it.toRetry)
}
}.launchIn(coroutineScope)
}
private fun pagedListBoundaryCallback(toRetry: Boolean) =
object : PagedList.BoundaryCallback<SomeData>() {
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
if (toRetry) {
coroutineScope.launch(coroutineDispatcherProvider.io()) {
loadNetwork(false)
}
}
}
2。使用TestCoroutineScope 运行测试。
SomeTest.kt
@ExperimentalCoroutinesApi
@Config(maxSdk = Build.VERSION_CODES.P, minSdk = Build.VERSION_CODES.P)
@RunWith(RobolectricTestRunner::class)
class SomeTest {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
@Test
fun someTest() = testScope.runBlockingTest {
...
}
3。使用runBlockingTest 运行测试。
@Test
fun someTest() = runBlockingTest {
...
}
4。使用 TestCoroutineDispatcher 上的 TestCoroutineScope 启动 Room 调用。
这不会导致主线程错误。但是,房间调用不适用于此方法。
@Test
fun topCafesTest() = testDispatcher.runBlockingTest {
testScope.launch(testDispatcher) {
dao.insertCafes(mockCafesList)
val cafesQuery = dao.queryCafes().toLiveData(PAGE_SIZE).asFlow()
cafesQuery.collect {
...
}
}
}
完整的错误日志
java.lang.IllegalStateException:无法访问主线程上的数据库,因为它可能会长时间锁定 UI。
在 androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:267) 在 androidx.room.RoomDatabase.beginTransaction(RoomDatabase.java:351) 在 app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:91) 在 app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:88) 在 androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:54) 在 kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 在 kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) 在 androidx.room.TransactionExecutor$1.run(TransactionExecutor.java:45) 在 kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) 在 kotlinx.coroutines.DispatcherExecutor.execute(Executors.kt:62) 在 androidx.room.TransactionExecutor.scheduleNext(TransactionExecutor.java:59) 在 androidx.room.TransactionExecutor.execute(TransactionExecutor.java:52) 在 kotlinx.coroutines.ExecutorCoroutineDispatcherBase.dispatch(Executors.kt:82) 在 kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) 在 kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在 kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166) 在 kotlinx.coroutines.BuildersKt.withContext(未知来源) 在 androidx.room.CoroutinesRoom$Companion.execute(CoroutinesRoom.kt:53) 在 androidx.room.CoroutinesRoom.execute(CoroutinesRoom.kt) 在 app.topcafes.feed.database.FeedDao_Impl.insertCafes(FeedDao_Impl.java:88) 在 app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:76) 在 app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) 在 kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) 在 kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 在 kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) 在 kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) 在 kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) 在 kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在 kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在 kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在 kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) 在 kotlinx.coroutines.BuildersKt.async(未知来源) 在 kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84) 在 kotlinx.coroutines.BuildersKt.async$default(未知来源) 在 kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) 在 kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) 在 app.topcafes.FeedTest.topCafesTest(FeedTest.kt:70) 在 sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 在 sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在 sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 在 java.lang.reflect.Method.invoke(Method.java:498) 在 org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) 在 org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 在 org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) 在 org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) 在 org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:546) 在 org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:252) 在 org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89) 在 java.util.concurrent.FutureTask.run(FutureTask.java:266) 在 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 在 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 在 java.lang.Thread.run(Thread.java:748)
【问题讨论】:
-
你能包括你的
Dao和你的queryData()方法吗? Room 对suspend insert()方法的支持已经关闭了主线程。 -
我已经更新了上面的代码示例以包含 Dao。另外,我尝试重构
queryData以实现suspend。编译器抛出error: Not sure how to convert a Cursor to this method's return type (androidx.paging.DataSource.Factory)。我已经完成了所有的级联queryData也可以调用suspend函数或从新的launch调用中启动函数,如上所示。
标签: android kotlin android-room android-testing junit5