androidandroid-roomkotlin-coroutinesandroid-unit-testing

Unit testing Room android - This job has not completed yet


I am currently unit testing my local data source which uses Room. I created a test class:

/**
 * Integration test for the [WatchListLocalDataSource].
 */
@RunWith(AndroidJUnit4::class)
@MediumTest
class WatchListLocalDataSourceTest {

    private lateinit var sut: WatchListLocalDataSourceImpl
    private lateinit var database: ShowsDatabase
    private lateinit var entityMapper: ShowEntityMapper

    private lateinit var testDispatcher: TestCoroutineDispatcher
    private lateinit var testScope: TestCoroutineScope

    @Before
    fun setup() {
        entityMapper = ShowEntityMapper()
        testDispatcher = TestCoroutineDispatcher()
        testScope = TestCoroutineScope(testDispatcher)
        val context = InstrumentationRegistry.getInstrumentation().context
        // using an in-memory database for testing, since it doesn't survive killing the process
        database = Room.inMemoryDatabaseBuilder(
            context,
            ShowsDatabase::class.java
        )
            .setTransactionExecutor(testDispatcher.asExecutor())
            .setQueryExecutor(testDispatcher.asExecutor())
            .build()

        sut = WatchListLocalDataSourceImpl(database.watchListDao(), entityMapper)
    }

    @After
    @Throws(IOException::class)
    fun cleanUp() {
        database.close()
    }

    @Test
    @Throws(Exception::class)
    fun observeWatchedShows_returnFlowOfDomainModel()  = testScope.runBlockingTest {

        val showId = 1
        sut.addToWatchList(mockShow(showId))

        val watchedShows: List<Show> = sut.observeWatchedShows().first()

        assertThat("Watched shows should contain one element", watchedShows.size == 1)
        assertThat("Watched shows element should be ${mockShow(showId).name}", watchedShows.first() == mockShow(showId))
    }
}

However, the test does not complete, noting:

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

The actual method in the sut is:

override suspend fun addToWatchList(show: Show) = withContext(Dispachers.IO) {
    watchListDao.insertShow(WatchedShow(entityMapper.mapFromDomainModel(show)))
}

Solution

  • So the problem started with the addToWatchList method in the data source where I explicitly differed it to the Dipachers.IO coroutine scope, this is unnecessary since Room handles the threading internally if you use the suspend keyword for you functions.

    This created a problem where the work started on the test coroutine scope was generating a new scope, and since room needs to complete on the same thread it was started on, there was a deadlock creating the java.lang.IllegalStateException: This job has not completed yet error.

    The solutions was:

    1. Remove withContext in the DAO insert method and let Room handle the scoping itself.
    2. add .allowMainThreadQueries() to the database builder in the @Before method of the test class, this allows room to work with the test scope provided and ensure all the work is conducted in that defined scope.

    Correct code is:

    @Before
    fun setup() {
        entityMapper = ShowEntityMapper()
        testDispatcher = TestCoroutineDispatcher()
        testScope = TestCoroutineScope(testDispatcher)
        val context = InstrumentationRegistry.getInstrumentation().context
        // using an in-memory database for testing, since it doesn't survive killing the process
        database = Room.inMemoryDatabaseBuilder(
            context,
            ShowsDatabase::class.java
        )
            .setTransactionExecutor(testDispatcher.asExecutor())
            .setQueryExecutor(testDispatcher.asExecutor())
    
            // Added this to the builder
                       |
                       v
    
            .allowMainThreadQueries()
    
            .build()
    
        sut = WatchListLocalDataSourceImpl(database.watchListDao(), entityMapper)
    }
    

    And in the dataSource class:

    override suspend fun addToWatchList(show: Show)  {
        watchListDao.insertShow(WatchedShow(entityMapper.mapFromDomainModel(show)))
    }