Android Testing Tutorial

Testing Room database with Coroutines and Flows — Testing Fundamentals

Always have room for change that flows!

Saurabh Pant
Dev Genius
Published in
5 min readAug 15, 2022

--

Source: https://testinggenez.com/

A common use case that we most of the time deal with is having a local database in our apps to support caching and using Room for this purpose is very well known. So writing tests for our database become very important at the same time because in many cases local database is the only source of data for the apps.

Most common operations using a database are CRUD operations. So in this article we’ll explore how can we write tests for such operations and that too when they returns us a flow.

We’ll have a simple app which takes an item with name and save it in db. So let’s begin.

Room Set up

First of all we need to set up the Room database in the app. So let’s quickly do that. Add the dependencies as follows.

Now create a table entity as follows.

Then define the Dao interface as follows. This is class where we define our database apis. Our add and delete functions are suspended.

Then create database entity as follows.

That’s it. The set up is done. Now we can quickly go through what we just set up. So we’ve defined three simple apis for our database which are add/update an item, delete an item and get all the items.

Test set up

To test our database set up, we need to understand a few points before hand which are

  • In test environment, instead of having a real database in the device storage, we create an in memory database or you can say a temporary database so that we can quickly test it and it won’t add any additional load on our device or app. It’ll be removed once the test is done.
  • Because we’re using flow to observe any database changes, it is tricky to observe flows in test environment. So we’ll use a library called Turbine. It makes easy to test flows. We need to add its dependency under androidTestImplementation as follows
androidTestImplementation 'app.cash.turbine:turbine:0.9.0'
  • As Room is an android library, we’ll write the tests in androidTest directory.

Let’s create the test class for our ItemDao.

We added two annotations on the class. @RunWith annotation tells the compiler to run this class with AndroidJUnit4 instead of JUnit apis.

@SmallTest annotation tells the compiler that it is test run frequently and it execution time should be less than 200ms. Test size qualifiers are a great way to structure test code and are used to assign a test to a test suite of similar run time.

Now we need to change our coroutine dispatcher from main to test dispatcher otherwise our tests will fail with exception. To do that, we create a rule and simply set the main dispatcher as test dispatcher and reset it when finished.

We add this rule in our ItemDaoTest.kt.

@get: Rule
val dispatcherRule = TestDispatcherRule()

Now let’s create our database and dao instances for test.

As mentioned already that we create an in memory instance of our database under the Before annotation and close our db in After annotation.

Alright! It’s time to write our tests finally. We’ll write the following test cases

  • adding two items in database and get all items flow should return both the items.
  • adding two items and deleting any of them. Our get all items flow should return only non-deleted item.
  • adding two items and updating any of them. Our get all items flow should return the updated items.

Write test cases

Test case 1

Adding an item in database, should be returned in our flow.

@Test
fun addItem_shouldReturn_theItem_inFlow() = runTest { }

Because our add function is a suspend function, we need to run it under the testing coroutine builder runTest.

We then create our items and add them to the database.

val item1 = Item(uid = 1, itemName = "item 1")
val item2 = Item(uid = 2, itemName = "item 2")
itemDao.addItem(item1)
itemDao.addItem(item2)

Now we test our flow using turbine library using the extension function test. We observe the emitted items list using awaitItem() function from turbine.

itemDao.getAllItems().test {
val list = awaitItem()
assert(list.contains(item1))
assert(list.contains(item2))
cancel()
}

Once we’ve the list, we assert if both the items are present in the list. If they’re then the test will pass otherwise fails.

Finally we call cancel() function, again from the turbine library.

This is an important catch here because if we won’t make this call then our flow will never completes and the test will run forever.
Flows should be finite in test environment and only then it can complete and we can test our assertions on the emitted values.

Running the above test will result as

Great! So we’ve successfully executed and tested our first test. Let’s now test deletion of item from database.

Test case 2

Deleting an item in database, should be returned in our flow.

@Test
fun deletedItem_shouldNot_be_present_inFlow() = runTest { }

To perform this test, we’ll first add two items in database as follows

val item1 = Item(uid = 1, itemName = "item 1")
val item2 = Item(uid = 2, itemName = "item 2")
itemDao.addItem(item1)
itemDao.addItem(item2)

Then we’ll perform deletion on second item (can do with either).

itemDao.removeItem(item2)

Now, we observe the flow and check if the deleted item is not present in the returned list of items as follows

itemDao.getAllItems().test {
val list = awaitItem()
assert(list.size == 1)
assert(list.contains(item1))
cancel()
}

Make sure to call cancel once we complete our assertions.

Run the test and the result will be as follows

And Bamn! We tested the deletion of item in database successfully.

Test case 3

Updating an item in database, should be returned in our flow.

@Test
fun updateItem_shouldReturn_theItem_inFlow() = runTest { }

If you’ve followed so far then you must have got sufficient understanding of how and what to test for our database. So now you can write the update item test case yourself and assert the condition.

Do comment and let me know if you’d be able to do it successfully and even if not. 🤘

Whoa! We’ve written test cases for our Room database along with testing our Flows. This would be enough to get started with testing and make your code more bug proof. Do the set up now and play around with different scenarios.

You can checkout the complete gist below for reference.

Bonus Read

That is all for now! Stay tuned!

Connect with me (if the content is helpful to you ) on

Subscribe to email to be in sync for further interesting topics on Android/IOS/Backend/Web.

Until next time…

Cheers!

--

--

App Developer (Native & Flutter) | Mentor | Writer | Youtuber @_zaqua | Traveller | Content Author & Course Instructor @Droidcon | Frontend Lead @MahilaMoney