How to write Unit Tests for the Database Access Object on Android

A DAO is a Data Access Object we create that facilitates working with a database. It is a common pattern when working with the Room library on Android.

When it comes to writing Unit Tests for the database Google recommends to use what’s called an in-memory database and execute the tests on an Android device.

val context = ApplicationProvider.getApplicationContext<Context>()
val database = Room.inMemoryDatabaseBuilder(
    context, TestDatabase::class.java).build()
val dao = database.getDao()

Dependencies

In this tutorial we’ll be adding a few Unit Tests for the TodosDao.kt file in our ImportantTodos app. We’re going to use the Room Testing library which provides helper methods for testing Room databases, as well as the Kotlinx Coroutines Test library which has helper methods for testing coroutines.

dependencies { 
    // Kotlin Coroutines testing support (we added this in the last tutorial)
    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0-RC2'

    // Since this needs to run on an Android device we must add this one to get the RunWith(AndroidJUnit4::class)
    androidTestImplementation "androidx.test.ext:junit:1.1.5"

    // To test LiveData we also need Arch Core
    androidTestImplementation "androidx.arch.core:core-testing:2.2.0"

    // Assertion library for more readable assertions
    androidTestImplementation "com.google.truth:truth:1.2.0"

    // Espresso Core - needed for AndroidJUnit4 runner
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}

No we can create a TodosDaoTest test class within the app/src/androidTest/java/com/example/importanttodos directory. Within the class we can add an InstantTaskExecutorRule() since we’re going to be dealing with LiveData and can also add the variables we’ll need to test: the TodosDao and the TodosDatabase.

private val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var testDatabase: TodosDatabase
private lateinit var testDao: TodosDao

Now let’s make sure we have a database instance before each test executes. We can do this by declaring a function that builds the database and annotate it with @Before.

@Before
fun createDatabase() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    testDatabase = Room.inMemoryDatabaseBuilder(
        context,
        TodosDatabase::class.java
    ).allowMainThreadQueries().build()
    testDao = testDatabase.todosDao
}

Note that for the purposes of testing (and only for testing) we want to allow queries to the database to occur on the Main thread. This is because queries to the database usually happen asynchronously and we also have a few LiveData properties that are updated using the values from those queries. To simplify tests and make sure that our data is available when we expect it within the test in order to make assertions, we allow the database queries to happen on the Main thread.

And we also want to add a function to close the database and annotate it with @After so that it is executed after each @Test method.

@After
fun closeDatabase() = testDatabase.close()

We can now begin to write our first test using camel snake case. For readability we can use the following pattern when writing test function names: <system under test> <expected result> <action taken>. We’ll also want to use the runTest function from the Coroutines Test library. I’ll first show you the “long” way of writing this test using an Observer and then we’ll go over Google’s recommended solution to avoid boilerplate code.

@Test
    @Throws(Exception::class)
    fun database_returnsOneItemAndCorrectTitle_afterInsertion() = runTest {
        val newTodo = Todo(
            1L,
            "Buy chocolate chip cookies 🍪",
            false
        )

        val observer = Observer<List<Todo>> {
            assertThat(it.size).isEqualTo(1)
            assertThat(it.first().title).isEqualTo(newTodo.title)
        }

        testDao.insert(newTodo)
        testDao.getAll().observeForever(observer)
        testDao.getAll().removeObserver(observer)
    }

This seems like a simple solution but having to create an Observer each time can get a little tedious. Because of this, Google suggests adding this LiveDataTestUtil.kt utility function to your codebase to use in these situations. There’s also a great blog post from Android Developers here with the full explanation of this utility. I’ll go ahead and add this to my androidTest directory and call it within my test as follows:

@Test
    @Throws(Exception::class)
    fun database_returnsOneItemAndCorrectTitle_afterInsertion() = runTest {
        val newTodo = Todo(
            1L,
            "Buy chocolate chip cookies 🍪",
            false
        )

        testDao.insert(newTodo)

        val todos = testDao.getAll().getOrAwaitValue()
        assertThat(todos.size).isEqualTo(1)

        val insertedTodo = testDao.get(1).getOrAwaitValue()
        assertThat(insertedTodo.title).isEqualTo(newTodo.title)
    }

Lastly we can add two more tests using our new getOrAwaitValue() util to validate the other methods from our TodosDao.kt as follows:

@Test
    @Throws(Exception::class)
    fun database_returnsZeroSize_afterAllItemsDeleted() = runTest {
        val newTodo = Todo(
            1L,
            "Buy peanut butter cookies 🥜",
            false
        )

        testDao.insert(newTodo)
        testDao.delete(newTodo)
        val todosListSize = testDao.getAll().getOrAwaitValue().size
        assertThat(todosListSize).isEqualTo(0)
    }

    @Test
    @Throws(Exception::class)
    fun database_returnsUpdatedTodoItem_afterUpdate() = runTest {
        val newTodo = Todo(
            1L,
            "Buy ice cream 🍦",
            false
        )

        testDao.insert(newTodo)
        newTodo.title = "Buy chocolate ice cream 🍦🍫"
        newTodo.completed = true
        testDao.update(newTodo)
        val updatedTodo = testDao.get(1).getOrAwaitValue()
        assertThat(updatedTodo.title).isEqualTo("Buy chocolate ice cream 🍦🍫")
        assertThat(updatedTodo.completed).isTrue()
    }

Hope you enjoyed this tutorial and please let me know if you have any feedback or ideas of other tutorials you’d like to see.

Happy coding! 😎

Daniel Perez-Gomez

Hi there! 👋 I'm an Android developer currently based in New York City. I write mostly about Android development using Kotlin as well as other programming bits. I'm committed to making this complex field fun and accessible to beginners through my guides and tutorials. I'm also driven by the belief in technology's power to enhance lives, which motivates me to develop apps that are both user-friendly and prioritize accessibility, catering to various needs. Additionally, I host a YouTube channel, “Daniel Talks Code”, where I break down complex concepts into easy-to-follow instructions. Join me in my quest to make the world of Android development inclusive and accessible for all!

Previous
Previous

Quick start guide to writing Espresso UI tests on Android - Android Testing Part 4

Next
Next

Android Unit Testing for Beginners