Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing Android Room with LiveData, Coroutines and Transactions

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:

  • Save some entities
  • Load the entities and assert the database mapping works as expected

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.
  • In order to observe the LiveData according to the above link, however, I must use the InstantTaskExecutorRule. (Otherwise I get java.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.

Detailed description

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.

like image 289
Martin Melka Avatar asked Jul 14 '19 13:07

Martin Melka


People also ask

How do I use LiveData with coroutines?

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.

What is runBlockingTest?

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.

How do you do the DAO test?

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.


1 Answers

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.

like image 171
Martin Melka Avatar answered Oct 02 '22 07:10

Martin Melka