【问题标题】:Room Local Unit Test - Query PagedList from DataSource.Factory房间本地单元测试 - 从 DataSource.Factory 查询 PagedList
【发布时间】:2020-10-04 08:27:57
【问题描述】:

问题

预期

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

观察到

Room 数据库@Insert@QueryTestCoroutineDispatcher().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"

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.ktqueryData 函数。

添加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


【解决方案1】:

Dispatchers.IO 上运行@Insert@Query

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() = testDispatcher.runBlockingTest {
        // Same Dispatcher, LiveData, and Room setup used as defined in the question above.

        testScope.launch(Dispatchers.IO) {
            // 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.
        ...
}

【讨论】:

    猜你喜欢
    • 2020-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-25
    • 2013-11-26
    • 2016-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多