Room Local Unit Test-'无法访问主线程上的数据库'

问题描述 投票:0回答:1

问题

预期

使用JUnit 5本地单元测试,在@Insert中运行Room数据库QueryTestCoroutineDispatcher()

已观察

房间数据库@Insert@QueryTestCoroutineDispatcher().runBlockingTest中执行,导致以下错误。如果使用非测试调度程序Dispatchers.IO明确定义了线程,则数据库调用将起作用。

错误日志:

无法访问主线程上的数据库,因为它可能长时间锁定UI。

实施

1。添加库

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中的数据库@QueryTestCoroutineDispatcher().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>
}

尝试的解决方案

1。将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是在...

android kotlin android-room android-testing junit5
1个回答
0
投票

@Insert上的运行室@QueryDispatchers.IO

© www.soinside.com 2019 - 2024. All rights reserved.