Room is a persistence library for Android that provides an abstraction layer over the SQLite database. With the help of the Room, we can easily create the database and perform CRUD operations.
For observing changes to the database and for receiving updates in a real-time Room can utilize Flow, an asynchronous data stream that sequentially emits data.
The basic components of Room are Entity, Dao and Database classes and for this testing we will use the Dao class because it contains all the necessary functions to access data in our database:
@Dao
interface ImageDao {
@Query("SELECT * FROM image_data_table")
fun getAllData(): Flow<List<ImageDataModel>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertData(imageDataModel: ImageDataModel): Long
@Update
suspend fun updateData(imageDataModel: ImageDataModel): Int
@Delete
suspend fun deleteData(imageDataModel: ImageDataModel): Int
@Query("DELETE FROM image_data_table")
suspend fun deleteAll(): Int
}
The recommended approach for testing database implementation is writing a JUnit test that runs on an Android device. Because these tests don’t require creating an activity, they should be faster to execute than UI tests.
In the setup() function we have to create new instances of Database for testing purpose. For creating a new instance of database we have to use Room.inMemoryDatabaseBuilder() instead of Room.databaseBuilder() we normally use. inMemoryDatabaseBuilder() creates an in memory version of database. Information stored in an in memory database disappears when the process is killed.
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
ImageDataBase::class.java)
.allowMainThreadQueries()
.build()
dao = database.imageDao()
}
For accessing data in the Flow we have to stop the thread. By using a CountDownLatch we can cause a thread to block until other threads have completed a given task. A CountDownLatch
is initialized with a given count value. We call the await() method of CountDownLatch to wait till the counter reaches 0, and then we execute our normal code flow. getAllData() method in Dao returns a Flow of a list of objects and the collect method is then used to observe the Flow and receive updates.
private fun getDataFromFlow() = runBlocking {
val latch = CountDownLatch(1)
val imageList = mutableListOf<ImageDataModel>()
val job = launch(Dispatchers.IO) {
dao.getAllData().collect { items ->
imageList.addAll(items)
latch.countDown()
}
}
withContext(Dispatchers.IO) {
latch.await(2, TimeUnit.SECONDS)
}
job.cancelAndJoin()
return@runBlocking imageList
}
And after that we can use return data for testing individual functions in Dao. For example:
@Test
fun update_Image_In_Db_should_contain_new_data() = runBlocking {
val imageItem = ImageDataModel(id = 1, "url1", "image1")
dao.insertData(imageItem)
val updatedImageItem = ImageDataModel(id = 1, "url2", "image2")
dao.updateData(updatedImageItem)
val imageList = getDataFromFlow()
assertTrue(imageList.contains(updatedImageItem))
}
or directly assert data inside .collect block of the code:
@Test
@Throws(Exception::class)
fun insert_Image_In_Db_Return_True() = runTest {
val imageItem = ImageDataModel(id = 1, "url1", "image1")
dao.insertData(imageItem)
val latch = CountDownLatch(1)
val job = launch(Dispatchers.IO) {
dao.getAllData().collect { items ->
assertTrue(items.contains(imageItem))
latch.countDown()
}
}
latch.await(2, TimeUnit.SECONDS)
job.cancel()
}
The complete code you can get here.