18

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.

Martin Melka
  • 7,177
  • 16
  • 79
  • 138
  • This is the only way to test room with live data and coroutine. Soon google will release new test lib to resolve such issues. – Hardik Bambhania Jul 16 '19 at 18:10
  • That's sad to hear. Do you happen to have a link to where they say they will resolve this issue? – Martin Melka Jul 22 '19 at 16:07
  • https://github.com/googlesamples/android-architecture-components/blob/master/BasicSample/app/src/androidTest/java/com/example/android/persistence/LiveDataTestUtil.java This is google sample code to test live data. – Hardik Bambhania Jul 22 '19 at 16:17
  • Another [related question](https://stackoverflow.com/q/56380210/6024687) – jL4 Aug 09 '19 at 04:33

2 Answers2

10

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.

Martin Melka
  • 7,177
  • 16
  • 79
  • 138
4

The problem is with the thing that transactions itself use runBlocking somewhere inside and that cause deadlock. I have changed InstantTaskExecutorRule to this class:

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)
    }
}

Then in code it will be:

@get:Rule
val liveDataRule = IsMainExecutorRule()

It will not cause deadlocks but still allow to observe livedatas.

vbevans94
  • 1,500
  • 1
  • 16
  • 15
  • This solution seems to fix the issue but there is no explanation of why exactly it works and no information whether it might affect some other lifecycle-related (e.g. LiveData) tests. If there are no downsides and side effects then why isn't it the default way to test? Can you elaborate @vbevans94? – Andrzej Zabost Dec 20 '19 at 13:34
  • It actually uses the default executor, but runs everything on one of 4 (this may differ with version) threads in a thread pool, which means that it may indeed solve the immediate problem, but any assumptions about main thread sequential execution is almost certainly wrong, and race conditions / non-synced data are likely problems. I would not go down this path. – Mikael Ohlson Oct 16 '21 at 14:28