Step #4: Testing Adds

Now, we can start putting in test logic for testing ToDoRepository itself.

Replace the ToDoRepositoryTest implementation with:

package com.commonsware.todo.repo

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.empty
import org.hamcrest.Matchers.equalTo
import org.hamcrest.collection.IsIterableContainingInOrder.contains
import org.junit.Assert.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ToDoRepositoryTest {
  @get:Rule
  val instantTaskExecutorRule = InstantTaskExecutorRule()

  private val context = InstrumentationRegistry.getInstrumentation().targetContext
  private val db = ToDoDatabase.newTestInstance(context)

  @Test
  fun canAddItems() = runBlockingTest {
    val underTest = ToDoRepository(db.todoStore(), this)
    val results = mutableListOf<List<ToDoModel>>()

    val itemsJob = launch {
      underTest.items().collect { results.add(it) }
    }

    assertThat(results.size, equalTo(1))
    assertThat(results[0], empty())

    val testModel = ToDoModel("test model")

    underTest.save(testModel)

    assertThat(results.size, equalTo(2))
    assertThat(results[1], contains(testModel))
    assertThat(underTest.find(testModel.id).first(), equalTo(testModel))

    itemsJob.cancel()
  }
}

Once again, we have a lot to explain.

@RunWith(AndroidJUnit4::class)

The @RunWith annotation gives JUnit a specific class to use to orchestrate running the test functions in this test class. For unit tests, by default we do not need to use this annotation, though certain libraries that you might use could require one. For instrumented tests, though, we need to point to a class that knows how to run the test and get the results off the device or emulator and over to the IDE. That is what AndroidJUnit4 helps with, in part. Unless you are using some other library that requires a different @RunWith annotation, all of your instrumented tests will start with this line.

  @get:Rule
  val instantTaskExecutorRule = InstantTaskExecutorRule()

This is another JUnit rule, one provided by the Jetpack testing library that we added to our dependencies. Like our MainDispatcherRule, InstantTaskExecutorRule ensures that our Room and other Jetpack asynchronous work really happens synchronously, to simplify our tests.

  private val context = InstrumentationRegistry.getInstrumentation().targetContext

We are going to need a Context to be able to set up our Room database. Specifically, we want a Context in the “context” of the code being tested (our app code). To get such a Context, we can ask an InstrumentationRegistry to give us an Instrumentation object representing our instrumented tests, and on there retrieve targetContext.

  private val db = ToDoDatabase.newTestInstance(context)

From there, we can set up a ToDoDatabase, using our newly-added newTestInstance() function and the context that we just obtained.

  @Test
  fun canAddItems() = runBlockingTest {

As with SingleModelMotorTest, we are going to be working with Kotlin coroutines. This time, though, we are running on Android, so we do not need to fuss with trying to change the nature of Dispatchers.Main. However, we do need to worry about ensuring that we have a CoroutineScope to use for our tests. In SingleModelMotorTest, we used runBlocking() where needed, and we used a TestCoroutineDispatcher inside of MainDispatcherRule. This time, we are using runBlockingTest(), which sets up a TestCoroutineDispatcher and uses that to have all of our coroutines run synchronously. Using runBlocking() has more flexibility; using runBlockingTest() frequently is simpler.

    val underTest = ToDoRepository(db.todoStore(), this)

We then can set up our ToDoRepository that we want to test. We use the DAO from our test ToDoDatabase for the first parameter. The second parameter — this — in the scope of runBlockingTest() is a TestCoroutineScope that we can use to manage the work done by our repository.

    val results = mutableListOf<List<ToDoModel>>()

    val itemsJob = launch {
      underTest.items().collect { results.add(it) }
    }

We then need to be able to see what gets put into the repository, to confirm that our changes to that database work. We already have an items() function to retrieve the items from the database. That is a Flow, emitting a new database result when we make changes to the database, in addition to emitting an initial result when we make the items() call. So, here, we manually collect() that flow, piping its results into a results object. results, therefore, is a List of our query results, with one element in the list per emission from the Flow. We hold onto the Job object created by launch(), because we need to cancel() that Job before the test completes — otherwise, runBlockingTest() will complain.

    assertThat(results.size, equalTo(1))
    assertThat(results[0], empty())

We then test to confirm that we got an initial result from our repository, and it shows that we have no entries in the database. assertThat(), equalTo(), and empty() are functions from Hamcrest, a testing library that we have access to via transitive dependencies from our other androidTestImplementation dependencies. Hamcrest, apparently named for a wave of cured pork products.

(the author of this book would like to point out that he is not responsible for naming these libraries)

Hamcrest is a large function library of “matchers” that, in the end, can perform some inspections of objects and return a boolean indicating whether the objects matched expectations or not. assertThat() uses those matchers, such as equalTo() and empty(), to examine and object and see if it meets expectations. Here, we are confirming that the results list has one element and that that element itself is an empty list.

    val testModel = ToDoModel("test model")

    underTest.save(testModel)

    assertThat(results.size, equalTo(2))
    assertThat(results[1], contains(testModel))
    assertThat(underTest.find(testModel.id).first(), equalTo(testModel))

We then:

Remember: ToDoModel is a data class. As a result, equality is based on the properties. We are not literally getting testModel back from Room — we are getting an equivalent model object, containing the same data.

    itemsJob.cancel()

Finally, we cancel() that Job that we set up, to make runBlockingTest() happy.

If you have a device or emulator set up, and you run ToDoRepositoryTest, you will see that canAddItems() succeeds.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.