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:
- Create a test model object
-
save()
that to the repository - Validate that we got another emission from the
Flow
, and that it contains our test model object - Validate that if we use
find()
to retrieve that model object based on itsid
value, that we get the model object back
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.