Android JUnit Tests for Room Database

While building Android applications, every developer comes across Room Persistence Library which provides an abstraction layer over SQLite. Good programmers tend to write tests for their Databases which make it easy for implementation as Android has made it as easy as possible for testing it. We will be covering the following topics today:

  1. Tests, Tests, Tests! Everywhere! Why?
  2. Grab the start project
  3. Dive into the start project
  4. What kind of tests?!
  5. Adding dependencies
  6. Adding utility functions
  7. Adding the tests

Tests, Tests, Tests! Everywhere! Why?

So, you’re a grown-up now. Life was already testing you, so you became an Android App Developer and tests started interfering again! When will they end? A simple answer to this question is, never. As tests help you become stronger, testing in Android makes your app implementation and data verification process easier. Talking about Room Database tests, they are recommended because they help define your data and verify your queries beforehand which means, you won’t need to build a repository, a ViewModel or UI to just see if your queries are executing as expected or not.

Grab the start project

Yes, Yes 🙄. I have provided you with some basic code but still, I’m gonna make you write code. I hate writing alone 😂! Either download the following repository or git clone it and open the project in Android Studio.

Dive into the start project

Let’s understand the start project. The project contains the following:

  1. Dependencies regarding Room runtime, compiler and testing. (build.gradle)(Module: app)
  2. A data class under data package labelled as Customer in which we have defined some fields.
  3. A CustomerDAO which is a Data Access Object class which contains all the queries we want to test
  4. A useless MainActivity and its related XML layout file

What kind of Tests?!

Test’s in your life right now…

via GIPHY

Okay, now come back to real life and let’s see what kind of tests we have. We will be creating the following tests for the Customer Data Access Object CustomerDAO class. We will be writing tests for verifying the data we will insert into the Database.

Adding Dependencies

Add the following dependencies in the build.gradle (Module: app) to enable Kotlin Room database extensions, Android lifecycle extensions and Core testing libraries for testing.

implementation "androidx.room:room-ktx:2.2.5"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"

Sync the project and let’s add the Room Database class. Create a package with the name db and create a Kotlin class with the name CustomerRoomDatabase and add the following code:

@Database(entities = [Customer::class], version = 1, exportSchema = false)
abstract class CustomerRoomDatabase : RoomDatabase() {

    abstract fun customerDAO(): CustomerDAO

}

We create an abstract class which extends the RoomDatabase class and assign the @Database annotation which takes the following 3 parameters

  1. entities – Data classes acting as the tables. For our Database, Customer is the table which we want to insert in the Database
  2. version – The version number of the database. Initially, we start with 1 and whenever we make a change to the entity or DAO we always increment this number.
  3. exportSchema – It looks for a boolean asking whether to export the Schema of the current database or not?

Within the class, we add an abstract function which returns the CustomerDAO. Now let’s add the utility functions we need for the tests.

Adding utility functions

We will be adding 2 utility functions. One for accessing the value from a DataType wrapped in LiveData and another function which adds data to the Database customer table. Create a new Object under the (androidTest) package with the name LiveDataTestUtil and enter the following function

@Throws(InterruptedException::class)
fun <T> getValue(liveData: LiveData<T>): T {
    val data = arrayOfNulls<Any>(1)
    val latch = CountDownLatch(1)
    val observer: Observer<T> = object : Observer<T> {
        override fun onChanged(t: T) {
            data[0] = t
            latch.countDown()
            liveData.removeObserver(this)
        }
    }
    liveData.observeForever(observer)
    latch.await(2, TimeUnit.SECONDS)
    @Suppress("UNCHECKED_CAST")
    return data[0] as T
}

This function is simply getting the value from the LiveData passed to it and now let’s add the function which adds the customers into the database.

fun addCustomers(mCustomerDAO: CustomerDAO) {

    var customer =
        Customer(fullName = "Bill Hoffman", age = 43, gender = 0, isCustomer = 0)
    mCustomerDAO.insertCustomer(customer)

    customer =
        Customer(fullName = "Catherine Jones", age = 19, gender = 2, isCustomer = 0)
    mCustomerDAO.insertCustomer(customer)

    customer =
        Customer(fullName = "Henry Williams", age = 28, gender = 2, isCustomer = 1)
    mCustomerDAO.insertCustomer(customer)

    customer = Customer(fullName = "John Smith", age = 21, gender = 0, isCustomer = 1)
    mCustomerDAO.insertCustomer(customer)

    customer =
        Customer(fullName = "Maria Garcia", age = 17, gender = 1, isCustomer = 0)
    mCustomerDAO.insertCustomer(customer)

    customer =
        Customer(fullName = "Martha Stewart", age = 33, gender = 1, isCustomer = 1)
    mCustomerDAO.insertCustomer(customer)

}

Why do we need to add customers firsthand? Because we are going to test the DAO queries based on this data. First, we create an object of the Customer class and then we add values within the constructor parameters and at last, we insert it into the database using the Data Access Object class which includes the insertCustomer function. So, one thing you might be wondering is why not use a boolean for isCustomer and string for gender? The SQLite Database doesn’t understand true or false whereas in the real world we can represent true with 1 and false with 0 or vice versa as you’re comfortable. And for the gender, we are using integers which we have already defined in the Customer class. To use them, we can pass the integer to a function which returns the specific gender string. We are saving memory 😉.

via GIPHY

Before we move further, here is what you have in LiveDataTestUtil

object LiveDataTestUtil {

    @Throws(InterruptedException::class)
    fun <T> getValue(liveData: LiveData<T>): T {
        val data = arrayOfNulls<Any>(1)
        val latch = CountDownLatch(1)
        val observer: Observer<T> = object : Observer<T> {
            override fun onChanged(t: T) {
                data[0] = t
                latch.countDown()
                liveData.removeObserver(this)
            }
        }
        liveData.observeForever(observer)
        latch.await(2, TimeUnit.SECONDS)
        @Suppress("UNCHECKED_CAST")
        return data[0] as T
    }

    fun addCustomers(mCustomerDAO: CustomerDAO) {

        var customer =
            Customer(fullName = "Bill Hoffman", age = 43, gender = 0, isCustomer = 0)
        mCustomerDAO.insertCustomer(customer)

        customer =
            Customer(fullName = "Catherine Jones", age = 19, gender = 2, isCustomer = 0)
        mCustomerDAO.insertCustomer(customer)

        customer =
            Customer(fullName = "Henry Williams", age = 28, gender = 2, isCustomer = 1)
        mCustomerDAO.insertCustomer(customer)

        customer = Customer(fullName = "John Smith", age = 21, gender = 0, isCustomer = 1)
        mCustomerDAO.insertCustomer(customer)

        customer =
            Customer(fullName = "Maria Garcia", age = 17, gender = 1, isCustomer = 0)
        mCustomerDAO.insertCustomer(customer)

        customer =
            Customer(fullName = "Martha Stewart", age = 33, gender = 1, isCustomer = 1)
        mCustomerDAO.insertCustomer(customer)

    }
}

Adding the tests

Are you getting bored? Awww! Come on. You’re learning something new. Let’s move forward by adding a new Kotlin class in the (androidTest) package with a name of CustomerDatabaseTest. Above the class name add the annotation @RunWith(AndroidJUnit4::class) so the IDE can understand that we are using it as an Android test class. The first thing we add in the class is a rule called Instant Task Executor Rule.

@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

InstantTaskExecutorRule swaps the background executor used by the Architecture Components with a different one which executes each task synchronously. Then we are going to add two properties.

private lateinit var mCustomerDAO: CustomerDAO
private lateinit var mCustomerDatabase: CustomerRoomDatabase

The first one is CustomerDAO which is the Data Access Object and the second one is the CustomerRoomDatabase. Why we need these? Let’s find out. Let’s create a function which executes before everything else.

@Before
fun createDatabase() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    mCustomerDatabase = Room.inMemoryDatabaseBuilder(context, CustomerRoomDatabase::class.java)
        .allowMainThreadQueries()
        .build()
    mCustomerDAO = mCustomerDatabase.customerDAO()
    LiveDataTestUtil.addCustomers(mCustomerDAO)
}

The @Before annotations execute the createDatabase() function before executing any test. Within the createDatabase() function we are getting the context from the ApplicationProvider class. Then, we initialize the mCustomerDatabase with the Room.inMemoryDatabaseBuilder constructor which takes in a context, a class which extends Room Database and using the builder pattern we allow main thread queries and then build it. We pass the context we got from the ApplicationProvider class and the CustomerDatabaseRoom class which we created earlier. The mCustomerDatabase variable will return the CustomerDatabase class from which we can access the customerDAO(). So, we initialize the mCustomerDAO by accessing the customerDAO() abstract function in the CustomerRoomDatabase class. In the end, we call the addCustomers() function from the LiveDataTestUtil class and pass the mCustomerDAO variable as an argument to add the data in the database. Confusing? Read it again.

via GIPHY

Moving forward, let’s add another function which will execute at the end.

@After
fun closeDatabase() {
    mCustomerDatabase.close()
}

The closeDatabase() will execute after executing all the test functions. It is important to close the database because it is a good practice and we don’t need to keep it in the memory as we are using the inMemoryDatabase system which creates a temporary instance of the Database in the memory. Keeping it open can lead to memory leaks or things we might not understand. So, you might be thinking, when are we going to write tests? Are we even going to write? Yes, now we are going to write so chin up and let’s start writing our first test.

Our first test will verify inserted data in the Database. First, we will get the value using the getAllCustomer() query function which returns a list of all customers using LiveData. Let’s write the function:

@Test
@Throws(Exception::class)
fun verifyInsertedData() {
    val mAllCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertEquals("Bill Hoffman", mAllCustomers.first().fullName)
}

We annotate the function with @Test to inform the IDE that this is a test and then using @Throws(Exception::class) we define the exception we can expect if the test fails. If we don’t know what kind of exception can occur, we use the Exception::class. Using the LiveDataTestUtil object we call the getValue function which takes in a variable wrapped in LiveData. Using the mCustomerDAO we call the getAllCustomer() query function which returns the list of all customers wrapped within LiveData. After that, we use the function assertEquals() which takes in multiple parameters and we are going to provide only 2. One is the expected value and the second is the actual value. The expected value and actual value are compared and if both are equal the function returns true which means the test passes and if not, then Woohoo! You’re even bad at writing tests (Kidding 😂!). Taking the example above, we expect a value of "Bill Hoffman" and for the actual value, we have given mAllCustomers.first().fullName which is the first element in the mAllCustomers. To run the test, click the play icon in the gutter before the function name. As both are equal, the test passes and if not, comment and tell us what error you face.

Test – Verifying deletion of all customers

@@Test
@Throws(Exception::class)
fun deleteAllCustomers() {
    mCustomerDAO.deleteAllCustomers()
    val mAllCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertEquals(mCustomerDAO.totalCustomers(), mAllCustomers.size)
    assertTrue(mAllCustomers.isEmpty())
}

As our customer table is pre-populated we need to call the deleteAllCustomers() function from the DAO which will delete all the customers. Then we get the list of all customers the same way we did above and using the assertEquals function we compare the count of total customers by using the query function (mCustomerDAO.totalCustomers()) provided in the DAO with the size of mAllCustomers.size which returns the size of the list. It’s simply common sense that when we delete all the data from a table it will return an empty list which means mAllCustomers.size is going to be zero so after running the test we can say, it’s true. We use another function assertTrue which takes in a condition and returns either True or False. Here we check if the list is empty or not. As all the data is deleted, the test should pass.

Test – Verifies deletion of a customer

@Test
@Throws(Exception::class)
fun deleteACustomer() {
    val mCustomerListBefore = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    mCustomerDAO.deleteACustomer(mCustomerListBefore[0])
    val mCustomerListAfter = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertEquals(mCustomerListBefore.size - 1, mCustomerListAfter.size)
}

Here we access all customers twice because the first time we access the customer list we have all the customers, nothing is deleted but when we access the list the second time we delete a customer from the list using the deleteACustomer query function in DAO which takes in a Customer object. The expected value in the assertEquals is obviously going to be one less in size of the after list and the actual value is the size of the after list.

Test – Verifies data from the getAllAdultCustomers() query

@Test
@Throws(Exception::class)
fun allAdultCustomers() {
    val mAllAdultCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllAdultCustomers())
    val mAllCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertNotEquals(mAllCustomers.size, mAllAdultCustomers.size)
    assertEquals("Bill Hoffman", mAllAdultCustomers.first().fullName)
}

First, we access the list of all adult customers and then all the customers. Then we use a new function called assertNotEquals which takes two objects and verifies they are not equal. We are comparing the size of both the lists we have because we have adult customers and teenagers too which means we are going to have a different list size. And in the last, using the assertEquals we compare the fullName of the first value from the adult customer list and "Bill Hoffman" which returns true.

Test – Verifies data from the getAllMaleCustomer() query

@Test
@Throws(Exception::class)
fun allMaleCustomers() {
    val mAllMaleCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllMaleCustomers())
    assertEquals(mCustomerDAO.totalMaleCustomers(), mAllMaleCustomers.size)
    assertEquals("John Smith", mAllMaleCustomers[1].fullName)
}

We access the list of all male customers and using the assertEquals we compare the count we get from totalMaleCustomers() query function and the list size of mAllMaleCustomers. Then we write another assertEquals to verify the fullName from the mAllMaleCustomers list to match "John Smith". If you read the code carefully, we are accessing the second element in the list.

Test – Verifies data from the getAllFemaleCustomers() query

@Test
@Throws(Exception::class)
fun allFemaleCustomers() {
    val mAllFemaleCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllFemaleCustomers())
    assertEquals(mCustomerDAO.totalFemaleCustomers(), mAllFemaleCustomers.size)
    assertEquals("Maria Garcia", mAllFemaleCustomers.first().fullName)
}

Here the query function is changed to test the data from getAllFemaleCustomers().

Test – Verifies data from the getAllFemaleCustomers() query

@Test
@Throws(Exception::class)
fun allOtherGenderCustomers() {
    val mAllOtherGenderCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllOtherGenderCustomers())
    assertEquals(mCustomerDAO.totalOtherGenderCustomers(), mAllOtherGenderCustomers.size)
    assertEquals("Catherine Jones", mAllOtherGenderCustomers.first().fullName)
}

Here the query function is changed to test the data from getAllOtherGenderCustomers().

Test – Verifies data from the getAllActiveCustomers() query

@Test
@Throws(Exception::class)
fun allActiveCustomers() {
    val mAllActiveCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllActiveCustomers())
    assertEquals(mCustomerDAO.allActiveCustomerCount(), mAllActiveCustomers.size)
    assertEquals("Maria Garcia", mAllActiveCustomers[2].fullName)
}

Here the query function is changed to test the data from getAllActiveCustomers(). Here, we access the third element’s fullName in the list of active customers for comparing.

Test – Verifies data from the allNonActiveCustomers() query

@Test
@Throws(Exception::class)
fun allNonActiveCustomers() {
    val mAllNonActiveCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllNonActiveCustomers())
    assertEquals(mCustomerDAO.allNonActiveCustomerCount(), mAllNonActiveCustomers.size)
    assertEquals("John Smith", mAllNonActiveCustomers[1].fullName)
}

Here the query function is changed to test the data from getAllNonActiveCustomers().

Test – Verify customer data updates using updateACustomer() query function

@Test
@Throws(Exception::class)
fun updateCustomerStatus() {
    val customer = Customer(id = 1, fullName = "Bill Hoffman", age = 43, gender = 0, isCustomer = 1)
    mCustomerDAO.updateACustomer(customer)
    val mAllCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertEquals(1, mAllCustomers.first().isCustomer)
}

Here we test the update query function which takes in a Customer object. First, we create an object and pass the id of the customer available in the database, so we’ve taken the first person on the list. We enter all the same values but for isCustomer we change it to 1, making Bill Hoffman a non-active customer. Then, using the updateACustomer query function we pass the customer object and then get all the customers. Using assertEquals we then compare 1 with the isCustomer value of the first element in the list. The test should pass as we have updated the value using the updateACustomer query function above.

Test – Verify the ID is incrementing

@Test
@Throws(Exception::class)
fun isIDIncrementing() {
    val mAllCustomers = LiveDataTestUtil.getValue(mCustomerDAO.getAllCustomer())
    assertEquals(1, mAllCustomers.first().id)
    assertEquals(2, mAllCustomers[1].id)
    assertEquals(3, mAllCustomers[2].id)
}

This is just a precautionary test which checks if the id which we haven’t provided while adding the customers, is incrementing or not. First, we get the list of all customers, then using assertEquals we check if the id of the first element is equal to 1 and moving further we check if the second element id is equal to 2 and in the last, we check if the third element id is equal to 3. If the test passes, the id is being incremented.

Test – Getting the eldest customer age (A FUN TEST)

@Test
@Throws(Exception::class)
fun eldestCustomer() {
    val eldest = mCustomerDAO.eldestCustomer()
    val maxAge = mCustomerDAO.listOfAges().max()
    assertEquals(maxAge, eldest)
}

We use the eldestCustomer() query function which gives us the age and then using another query function listOfAges() which returns the list of all ages from the customer table, we find the max() (The greatest value in the list) and compare both the values in assertEquals. The amazing part for you all is that it’s the last test. Yoho! Let’s do a quick dance!

via GIPHY

Why we need these kinds of tests? These are the basic functionalities of the Room Database Query functions and they should work. Yes, they will work 90% of the time but it’s practice what makes a man perfect. So, writing these tests will help you understand how tests are written. If you’re willing to just look at the end-project, you require git. So, open the terminal and enter the following command to access the end-project

git checkout end-project

Congratulations on making it to the end. If you have any queries, comment or email us at [email protected]