使用JUnit 5本地单元测试,在@Insert
中运行Room数据库Query
和TestCoroutineDispatcher()
。
房间数据库@Insert
和@Query
在TestCoroutineDispatcher().runBlockingTest
中执行,导致以下错误。如果使用非测试调度程序Dispatchers.IO
明确定义了线程,则数据库调用将起作用。
错误日志:
无法访问主线程上的数据库,因为它可能长时间锁定UI。
实施
1。添加库
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"
2。创建测试
a。设置测试分派器和LiveData执行程序。
b。创建测试数据库:Test and debug your database。
c。确保测试数据库在与单元测试相同的Dispatcher上执行:Testing AndroidX Room + Kotlin Coroutines-@Eyal Guthmann
d。运行@Insert
中的数据库@Query
和TestCoroutineDispatcher().runBlockingTest
。
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> }
尝试的解决方案
suspend
修饰符添加到SomeDao.kt的queryData
函数。[添加suspend
之后,随后调用queryData
的方法必须也实现suspend
或使用launch
从协程启动,如下所示。
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(本机方法)处 在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)
预期的问题使用JUnit 5本地单元测试,在TestCoroutineDispatcher()中运行Room数据库@Insert和Query。已观察到Room数据库@Insert和@Query是在...
@Insert
上的运行室@Query
和Dispatchers.IO