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)))
}
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:
withContext
in the DAO insert method and let Room handle the scoping itself..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)))
}