使用LiveData,协程和事务测试Android Room [英] Testing Android Room with LiveData, Coroutines and Transactions
问题描述
我想测试我的数据库层,我陷入了22类情况.
I want to test my database layer and I have caught myself in a catch-22 type of a situation.
测试用例由两部分组成:
- 保存一些实体
- 加载实体并断言数据库映射按预期工作
总而言之,问题在于:
-
Insert
是suspend
方法,这意味着它需要在runBlocking{}
中运行
-
Query
返回结果的LiveData
,该结果也是异步的.因此,需要对其进行观察.有一个此 SO问题,说明了如何执行此操作. - 为了根据上面的链接观察LiveData,我必须使用
InstantTaskExecutorRule
. (否则我会得到java.lang.IllegalStateException: Cannot invoke observeForever on a background thread.
) - 这适用于大多数情况,但不适用于
@Transaction
注释的DAO方法.测试永远不会结束.我认为它在等待某些事务线程时陷入僵局. - 删除
InstantTaskExecutorRule
可以使Transaction-Insert方法完成,但是我不能断言其结果,因为我需要规则才能观察数据.
Insert
is asuspend
method, which means it needs to be run inrunBlocking{}
Query
returns aLiveData
of the result, which is also asynchronous. Therefore it needs to be observed. There's this SO question that explains how to do that.- In order to observe the LiveData according to the above link, however, I must use the
InstantTaskExecutorRule
. (Otherwise I getjava.lang.IllegalStateException: Cannot invoke observeForever on a background thread.
) - This works for most of the cases, but it does not work with
@Transaction
-annotated DAO methods. The test never finishes. I think it's deadlocked on waiting for some transaction thread. - Removing the
InstantTaskExecutorRule
lets the Transaction-Insert method finish, but then I am not able to assert its results, because I need the rule to be able to observe the data.
我的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
}
}
getValueBlocking
是LiveData
的扩展功能,几乎是从上面的链接复制粘贴的
The getValueBlocking
is an extension function for LiveData
, pretty much copypasted from the link above
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
}
测试此方案的正确方法是什么?在开发数据库映射层时,我需要进行这些类型的测试,以确保一切正常.
What's the proper way to test this scenario? I need these types of tests while developing the database mapping layer to make sure everything works as I expect.
推荐答案
There is now a solution to this issue, explained in this answer.
此修复程序将单个行添加到房间"内存数据库构建器:
The fix is adding a single line to the Room in-memory database builder:
db = Room
.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.setTransactionExecutor(Executors.newSingleThreadExecutor()) // <-- this makes all the difference
.build()
使用单线程执行程序,测试将按预期工作.
With the single thread executor the tests are working as expected.
这篇关于使用LiveData,协程和事务测试Android Room的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!