I want to test my database layer and I have caught myself in a catch-22 type of a situation.
The test case consists of two things:
The problem, in short, is that:
Insert
is a suspend
method, which means it needs to be run in runBlocking{}
Query
returns a LiveData
of the result, which is also asynchronous. Therefore it needs to be observed. There's this SO question that explains how to do that.InstantTaskExecutorRule
. (Otherwise I get java.lang.IllegalStateException: Cannot invoke observeForever on a background thread.
)@Transaction
-annotated DAO methods. The test never finishes. I think it's deadlocked on waiting for some transaction thread.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.My Dao
class looks like this:
@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)
}
}
The test case is:
@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
}
}
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.
ViewModel + LiveData You could use a MutableLiveData like so: But, since you will be exposing this result to your view, you can save some typing by using the liveData coroutine builder which launches a coroutine and lets you expose results through an immutable LiveData. You use emit() to send updates to it.
runBlockingTest takes in a block of code and blocks the test thread until all of the coroutines it starts are finished. It also runs the code in the coroutines immediately (skipping any calls to delay ) and in the order they are called–-in short, it runs them in a deterministic order.
Enzyme test: Dao is an analytical test which is carried out in the laboratory using the ELISA method to measure the level of the DAO enzyme in the blood and thus to identify whether the migraine is caused by a deficit in DAO. You should fast for a minimum of eight hours prior to the extraction of blood.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With