Room added support for Kotlin coroutines in v2.1. This is great as it now gives us an easy way to get data out of our database without worrying to much about threading.

For example, your may create this simple user storage DAO:

In this example we declare a suspend fun setUserEntity(userEntity: UserEntity) for writing and suspend fun getUser(): UserEntity? for reading the current user. Both of these are suspend function meaning they will run in their own coroutine without us having to worry about thread hopping to read from disk!

Now let’s say we want to be good engineers and write a test for our code. We’ll start by googling around and will probably come across kotlinx-coroutines-test. The basic usage says to wrap your code in runBlockingTest so that all your coroutines will run in the test scope (and you can also do cool things like call dalay from the test without actually waiting). Our code will therefore will look something like This:

However when we run the test we get:

java.lang.IllegalStateException: This job has not completed yet

This exception usually means that some coroutines from your tests were schedules outside the test scope (more specifically the test dispatcher).

If we dig around a little bit, we’ll discover that Room allows you to specify Query and Transaction Executors. Bingo! These default are probably causing our calls into room to run outside the test scope!.

In order to fix our test we need to use a few more advanced options from Room and Coroutines. First we need to create a TestCoroutineDispatcher (Note — according to the documentation it’s slightly better to use TestCoroutineScope for better error handling, so we’ll just wrap our dispatcher in a scope):

private val testDispatcher = TestCoroutineDispatcher()

private val testScope = TestCoroutineScope(testDispatcher)

next, we’ll need to pass this dispatcher to Room. Room only accept an executor. Fortunately, Kotlin coroutines come with a handy CoroutineDispatcher.asExecutor extension function. We therefore update out builder as follows:

Room.inMemoryDatabaseBuilder(context, UserDatabase::class.java)

.setTransactionExecutor(testDispatcher.asExecutor())

.setQueryExecutor(testDispatcher.asExecutor())

(note — if you read through Room’s code it’s technically possible to just set one executor, but why not set both!)

Finally, we update out test to run under the named TestCoroutineScope by changing runBlockingTest to testScope.runBlockingTest

Voila! Now our test passes! The final version of the test will look something like:

Bonus: Using Flow

For simplicity’s sake I showed who to read values from disk using a suspend function. Room also support observing data using Kotlin’s new Flow API. This is a much better way of reading data as we can actually reactively observe changes in the db.

Using Flow, we will replace getUser with a Flow property:

@get:Query("SELECT * FROM UserEntity WHERE userType='Personal' LIMIT 1")

val user: Flow<UserEntity?>

and the test will look like:

t

Happy testing!

Limitation of this approach — Deadlocks when using transactions

Because the way Room gets around android sqlite’s threading model, this solution will cause a deadlock if you’re using transactions (either by using @Transaction annotation or by using withTransaction function). This is because Room needs to take over a thread from the dispatcher for the duration of a transaction. Given that a TestCoroutineDispatcher is single threaded (it runs all the coroutines on the test thread), that means we will have no other thread to run the blocking coroutine on and the transaction (and test thread) will hang indefinitely.

Unfortunately, TestCoroutineDispatcher does not support using another dispatcher to schedule any of the test coroutines. In-fact, if any of your coroutines get scheduled on another thread your test will end prematurely with a useless stack trace like this:

java.lang.IllegalStateException: This job has not completed yet

at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1188)

at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53)

at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:72)

at app.eyal.myapp.auth.UserStorageTest.basicTest(UserStorageTest.kt:49)

...

The reason for this exception is that runBlockingTest decides your test is done when your TestCoroutineDispatcher goes idle and not when your test block finishes. If you have coroutines scheduled on another scheduler then runBlockingTest mistakably thinks your test is done before it’s actually done (This should be fixed soon).

For now the only workaround for transaction is to give up on using TestCoroutineDispatcher and to use runBlocking for your test rather than runBlockingTest .

Thanks Jon F Hancock for pointing it out and to Mike Nakhimovich and Yigit Boyar for the article about Room’s threading model.