简体   繁体   English

房间本地单元测试 - 从 DataSource.Factory 查询 PagedList

[英]Room Local Unit Test - Query PagedList from DataSource.Factory

Issue问题

Expected预期的

Using a JUnit 5 local unit test, run a Room database @Insert and Query within a TestCoroutineDispatcher() .使用 JUnit 5 本地单元测试,在TestCoroutineDispatcher()中运行 Room 数据库@InsertQuery

Observed观察到的

The Room database @Insert and @Query is executed within TestCoroutineDispatcher().runBlockingTest , causing the error below. Room 数据库@Insert@QueryTestCoroutineDispatcher().runBlockingTest中执行,导致以下错误。 The database calls will work if the threading is explicitly defined with the non-test dispatcher, Dispatchers.IO .如果使用非测试调度程序Dispatchers.IO显式定义线程,则数据库调用将起作用。

Error log:错误日志:

Cannot access database on the main thread since it may potentially lock the UI for a long period of time.无法访问主线程上的数据库,因为它可能会长时间锁定 UI。

Implement实施

1. Add libraries 1.添加库

build.gradle (SomeProjectName) build.gradle (SomeProjectName)

dependencies {
    ...
    // JUnit 5
    classpath("de.mannodermaus.gradle.plugins:android-junit5:X.X.X")
}

build.gradle (:someModuleName) 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. Create test 2.创建测试

a.一个。 Set test Dispatcher and LiveData executor.设置测试 Dispatcher 和 LiveData 执行器。

b.湾。 Create a test database: Test and debug your database .创建测试数据库: 测试和调试您的数据库

c. c。 Ensure the test database executes on the same Dispatcher as the unit test: Testing AndroidX Room + Kotlin Coroutines - @Eyal Guthmann确保测试数据库在与单元测试相同的 Dispatcher 上执行: Testing AndroidX Room + Kotlin Coroutines - @Eyal Guthmann

d. d。 Run the database @Insert and @Query within TestCoroutineDispatcher().runBlockingTest .TestCoroutineDispatcher().runBlockingTest中运行数据库@Insert@Query

SomeTest.kt 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 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>
}

Attempted solutions尝试的解决方案

1. Add suspend modifier to SomeDao.kt 's queryData function. 1. 在SomeDao.ktqueryData function 中添加suspend修饰符。

After adding suspend , the methods that subsequently call queryData must either implement suspend as well or be launched using launch , from a coroutine, as shown below.添加suspend之后,随后调用queryData的方法必须要么实现suspend ,要么使用launch ,从协程启动,如下所示。

This results in an error from the compiler.这会导致编译器出错。

error: Not sure how to convert a Cursor to this method's return type (androidx.paging.DataSource.Factory<{SomeDataClassPathHere}>).错误:不确定如何将 Cursor 转换为此方法的返回类型 (androidx.paging.DataSource.Factory<{SomeDataClassPathHere}>)。

SomeDao.kt SomeDao.kt

@Dao
interface SomeDao {
    ...
    @Query("SELECT * FROM someDataTable")
    suspend fun queryData(): DataSource.Factory<Int, SomeData>
}

SomeRepo.kt 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 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. Run the test with TestCoroutineScope . 2. 使用TestCoroutineScope运行测试。

SomeTest.kt 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. Run the test with runBlockingTest . 3. 使用runBlockingTest运行测试。

    @Test
    fun someTest() = runBlockingTest {
        ... 
    }

4. Launch the Room calls using TestCoroutineScope on TestCoroutineDispatcher. 4. 使用 TestCoroutineDispatcher 上的 TestCoroutineScope 启动 Room 调用。

This doesn't cause a main thread error.这不会导致主线程错误。 However, the Room calls do not work with this method.但是,房间调用不适用于此方法。

    @Test
    fun topCafesTest() = testDispatcher.runBlockingTest {
        testScope.launch(testDispatcher) {
            dao.insertCafes(mockCafesList)
            val cafesQuery = dao.queryCafes().toLiveData(PAGE_SIZE).asFlow()
            cafesQuery.collect {
                ...
            }
        }
    }

Full error log完整的错误日志

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time. java.lang.IllegalStateException:无法访问主线程上的数据库,因为它可能会长时间锁定 UI。

at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:267) at androidx.room.RoomDatabase.beginTransaction(RoomDatabase.java:351) at app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:91) at app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:88) at androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:54) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at androidx.room.TransactionExecutor$1.run(TransactionExecutor.java:45) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) at kotlinx.coroutines.DispatcherExecutor.execute(Executors.kt:62) at androidx.room.TransactionExecut at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:267) at androidx.room.RoomDatabase.beginTransaction(RoomDatabase.java:351) at app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:91) at app.topcafes.feed.database.FeedDao_Impl$2.call(FeedDao_Impl.java:88) at androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:54) at kotlin.coroutines.jvm.internal.BaseContinuationImpl .resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at androidx.room.TransactionExecutor$1.run(TransactionExecutor.java:45) at kotlinx.coroutines.test.TestCoroutineDispatcher.p (TestCoroutineDispatcher.kt:50) 在 kotlinx.coroutines.DispatcherExecutor.execute(Executors.kt:62) 在 androidx.room.TransactionExecut or.scheduleNext(TransactionExecutor.java:59) at androidx.room.TransactionExecutor.execute(TransactionExecutor.java:52) at kotlinx.coroutines.ExecutorCoroutineDispatcherBase.dispatch(Executors.kt:82) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166) at kotlinx.coroutines.BuildersKt.withContext(Unknown Source) at androidx.room.CoroutinesRoom$Companion.execute(CoroutinesRoom.kt:53) at androidx.room.CoroutinesRoom.execute(CoroutinesRoom.kt) at app.topcafes.feed.database.FeedDao_Impl.insertCafes(FeedDao_Impl.java:88) at app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:76) at app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) at kotlinx.coroutines.test.TestB or.scheduleNext(TransactionExecutor.java:59) at androidx.room.TransactionExecutor.execute(TransactionExecutor.java:52) at kotlinx.coroutines.ExecutorCoroutineDispatcherBase.dispatch(Executors.kt:82) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation .kt:288) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166) at kotlinx.coroutines.BuildersKt.withContext(Unknown Source ) 在 androidx.room.CoroutinesRoom$Companion.execute(CoroutinesRoom.kt:53) 在 androidx.room.CoroutinesRoom.execute(CoroutinesRoom.kt) 在 app.topcafes.feed.database.FeedDao_Impl.insertCafes(FeedDao_Impl.Z93F725A07423FE1C889F448B33D21F46)在 app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:76) 在 app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) 在 kotlinx.coroutines.test.TestB uildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) at kotlinx.coroutines.BuildersKt.async(Unknown Source) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84) at kotlinx.coroutines.BuildersKt.async$default(Unknown Source) at kotl uildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at 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。 kotlinx.coroutines.BuildersKt.async$default(Unknown Source) at kotlinx.coroutines.BuildersKt.async(Unknown Source) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84) inx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) at app.topcafes.FeedTest.topCafesTest(FeedTest.kt:70) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit inx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) at app.topcafes.FeedTest.topCafesTest(FeedTest.kt:70) at sun .reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke (Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org .junit .runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:546) at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:252) at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Thre .runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner. java:546) at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:252) at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89) at java. util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Thre adPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) adPoolExecutor.java:624) 在 java.lang.Thread.run(Thread.java:748)

Run Room @Insert and @Query on Dispatchers.IODispatchers.IO上运行 Room @Insert@Query

SomeTest.kt 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.
        ...
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 具有多视图 RecyclerView 的 Room DataSource.Factory - Room DataSource.Factory with multiple view RecyclerView Android 分页库 - Map 房间 DataSource.Factory&lt;*, DatabaseModel&gt; 到 DataSource.Factory&lt;*, PresenterModel&gt; - Android Paging Library - Map Room DataSource.Factory<*, DatabaseModel> to DataSource.Factory<*, PresenterModel> 如何从 DataSource.Factory 获取数据 - How to get data from DataSource.Factory Kotlin 协程挂起错误与 Room DataSource.Factory - Kotlin coroutine suspend error with Room DataSource.Factory Android Room 在使用 DataSource.Factory 时如何按文本搜索? - Android Room how to search by text when using DataSource.Factory? 我可以将 SimpleSQLiteQuery object 投射到 DataSource.Factory<key, value> 而不是 LiveData <pagedlist<> ? </pagedlist<></key,> - Can i cast SimpleSQLiteQuery object to DataSource.Factory<key, value> and than LiveData<PagedList<>? 如何测试返回DataSource.Factory的Dao方法? - How to test Dao methods which return DataSource.Factory? 无法从Android的分页中的DataSource.Factory获得toLiveData - toLiveData not available from DataSource.Factory in Android's Paging Android体系结构组件分页DataSource.Factory错误 - Android architecture components paging DataSource.Factory error DataSource.Factory的create方法不会得到调用 - create method of DataSource.Factory doesn't get call
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM