【问题标题】:Testing Android Room with LiveData, Coroutines and Transactions使用 LiveData、协程和事务测试 Android Room
【发布时间】:2019-11-23 10:54:03
【问题描述】:

我想测试我的数据库层,但我发现自己陷入了一种 catch-22 类型的情况。

测试用例由两部分组成:

  • 保存一些实体
  • 加载实体并断言数据库映射按预期工作

简而言之,问题在于:

  • Insert 是一个suspend 方法,这意味着它需要在runBlocking{} 中运行
  • Query 返回结果的LiveData,这也是异步的。因此需要观察。 this SO 问题解释了如何做到这一点。
  • 但是,为了根据上面的链接观察 LiveData,我必须使用InstantTaskExecutorRule。 (否则我会得到java.lang.IllegalStateException: Cannot invoke observeForever on a background thread.
  • 这适用于大多数情况,但不适用于@Transaction-annotated DAO 方法。测试永远不会完成。我认为它在等待某个事务线程时陷入僵局。
  • 删除 InstantTaskExecutorRule 让 Transaction-Insert 方法完成,但我无法断言其结果,因为我需要规则才能观察数据。

详细说明

我的Dao 类如下所示:

@Dao
interface GameDao {
    @Query("SELECT * FROM game")
    fun getAll(): LiveData<List<Game>>

    @Insert
    suspend fun insert(game: Game): Long

    @Insert
    suspend fun insertRound(round: RoundRoom)

    @Transaction
    suspend fun insertGameAndRounds(game: Game, rounds: List<RoundRoom>) {
        val gameId = insert(game)
        rounds.onEach {
            it.gameId = gameId
        }

        rounds.forEach {
            insertRound(it)
        }
    }

测试用例是:

@RunWith(AndroidJUnit4::class)
class RoomTest {
    private lateinit var gameDao: GameDao
    private lateinit var db: AppDatabase

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(
            context, AppDatabase::class.java
        ).build()
        gameDao = db.gameDao()
    }

    @Test
    @Throws(Exception::class)
    fun storeAndReadGame() {
        val game = Game(...)

        runBlocking {
            gameDao.insert(game)
        }

        val allGames = gameDao.getAll()

        // the .getValueBlocking cannot be run on the background thread - needs the InstantTaskExecutorRule
        val result = allGames.getValueBlocking() ?: throw InvalidObjectException("null returned as games")

        // some assertions about the result here
    }

    @Test
    fun storeAndReadGameLinkedWithRound() {
        val game = Game(...)

        val rounds = listOf(
            Round(...),
            Round(...),
            Round(...)
        )

        runBlocking {
            // This is where the execution freezes when InstantTaskExecutorRule is used
            gameDao.insertGameAndRounds(game, rounds)
        }

        // retrieve the data, assert on it, etc
    }
}

getValueBlockingLiveData 的扩展函数,几乎从上面的链接复制粘贴

fun <T> LiveData<T>.getValueBlocking(): T? {
    var value: T? = null
    val latch = CountDownLatch(1)

    val observer = Observer<T> { t ->
        value = t
        latch.countDown()
    }

    observeForever(observer)

    latch.await(2, TimeUnit.SECONDS)
    return value
}

测试这种情况的正确方法是什么?在开发数据库映射层时,我需要这些类型的测试,以确保一切都按预期工作。

【问题讨论】:

标签: android testing kotlin android-room kotlin-coroutines


【解决方案1】:

现在有解决此问题的方法,在 this answer 中进行了说明。

修复方法是在 Room 内存数据库构建器中添加一行:

db = Room
    .inMemoryDatabaseBuilder(context, AppDatabase::class.java)
    .setTransactionExecutor(Executors.newSingleThreadExecutor()) // <-- this makes all the difference
    .build()

使用单线程执行器,测试按预期工作。

【讨论】:

    【解决方案2】:

    问题在于事务本身在内部某处使用 runBlocking 并导致死锁。 我已将 InstantTaskExecutorRule 更改为此类:

    class IsMainExecutorRule : TestWatcher() {
    
        val defaultExecutor = DefaultTaskExecutor()
    
        override fun starting(description: Description?) {
            super.starting(description)
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    defaultExecutor.executeOnDiskIO(runnable)
                }
    
                override fun postToMainThread(runnable: Runnable) {
                    defaultExecutor.executeOnDiskIO(runnable)
                }
    
                override fun isMainThread(): Boolean {
                    return true
                }
            })
        }
    
        override fun finished(description: Description?) {
            super.finished(description)
            ArchTaskExecutor.getInstance().setDelegate(null)
        }
    }
    

    然后在代码中它将是:

    @get:Rule
    val liveDataRule = IsMainExecutorRule()
    

    它不会导致死锁,但仍然允许观察实时数据。

    【讨论】:

    • 这个解决方案似乎解决了这个问题,但没有解释它为什么会起作用,也没有信息它是否会影响其他一些与生命周期相关的(例如 LiveData)测试。如果没有缺点和副作用,那么为什么它不是默认的测试方式?你能详细说明@vbevans94 吗?
    • 它实际上使用默认的执行器,但是在线程池中的 4 个(这可能与版本不同)线程之一上运行所有内容,这意味着它确实可以解决眼前的问题,但任何关于主线程顺序执行几乎肯定是错误的,竞争条件/非同步数据可能是问题。我不会走这条路。
    猜你喜欢
    • 1970-01-01
    • 2021-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-08
    • 2020-04-12
    • 2020-05-07
    • 2021-09-24
    相关资源
    最近更新 更多