Before implementing the SQLite database in our activity TodoListActivity, we will test the various CRUD calls, and verify that they're working correctly. This will also allow us to validate our DAO interfaces with various tests. 🙂
Create a test class
To begin testing our database's tables, we will focus on the class ItemDAO
, and validate most of its CRUD methods. For this, we will create a class called ItemDaoTest
in the folder dedicated to our instrumented tests, androidTest/, so that they are run from an Android device (rather than on the JVM).
But first, we must install a small library from Android, which will make it easier to implement our tests. Do this by inputting the command below:
Excerpt of build.gradle
dependencies {
...
// TESTING
androidTestImplementation "android.arch.core:core-testing:1.1.1"
}
Perfect! Now let’s create a class responsible for the tests carried out on ItemDAO. This test class will be instrumented and run using AndroidJUnitRunner via the annotation @RunWith(AndroidJUnit4.class)
. This test runner will load the package containing all of our tests on one Android device to run them:
Class ItemDaoTest.kt:
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
// FOR DATA
private lateinit var database: SaveMyTripDatabase = null
@Rule @JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
@Throws(Exception::class)
fun initDb() {
this.database = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
SaveMyTripDatabase::class.java)
.allowMainThreadQueries()
.build()
}
@After
@Throws(Exception::class)
fun closeDb() {
database.close()
}
}
Then we defined a rule using the annotation @Rule
. As a reminder, a rule allows you to define the manner in which the tests will be executed. In our case, we have used the InstantTaskExecutorRule to force the execution of each test synchronously (i.e., without moving them to a thread in the background).
Next, we created a method initDb()
, which will load to create an instance of our database, then place it in the variable database declared at the top of our class. This method will be called before each test is run, using the annotation @Before
.
Okay, but that's weird. The builder to generate our Room class is strange.
Ah! You noticed! 🙂 Yes, to facilitate the unit tests, Room provides us with a builder called inMemoryDatabaseBuilder. This builder creates an instance of our database directly in memory (rather than in a file on a device!). Isn't that handy?
Testing CRUD calls
Now let’s move on to the tests. First, create (in your test package) a utility object that will enable us to more easily run methods that return values of the type LiveData.
Object LiveDataTestUtil.kt
object LiveDataTestUtil {
@Throws(InterruptedException::class)
fun <T> getValue(liveData: LiveData<T>: T {
val data = arrayOfNulls<Any>(1)
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return data[0] as T
}
}
This class is provided by Google to help you create tests involving the type LiveData more easily, and especially to block the test’s execution until the result has been returned.
First, let’s test the adding and retrieval of a new user in our SQLite database.
Excerpt from ItemDaoTest
@RunWith(AndroidJUnit4.class)
class ItemDaoTest {
...
// DATA SET FOR TEST
private val USER_ID: Long = 1
private val USER_DEMO = User(USER_ID, "Philippe", "https://www.google.fr, ")
private val NEW_ITEM_PLACE_TO_VISIT = Item("Visit this dream place !", 0, USER_ID)
private val NEW_ITEM_IDEA = Item("We could do dog sledding ?", 1, USER_ID)
private val NEW_ITEM_RESTAURANTS = Item("This restaurant looks nice", 2, USER_ID)
@Test
@Throws(InterruptedException::class)
fun insertAndGetUser() {
// BEFORE : Adding a new user
this.database.userDao().createUser(USER_DEMO)
// TEST
val user = LiveDataTestUtil.getValue(this.database.userDao().getUser(USER_ID))
assertTrue(user.username == USER_DEMO.username && user.id == USER_ID)
}
Before creating a test, declare and instantiate a dataset that you will likely reuse in your various tests. Here we're simply creating a demo user. 🙂
Next, create the first test with the annotation @Test
. This test will first insert a new user into the database (with the DAO’s method createUser), then retrieve it; this time, from the same database, using the getUser method.
Once the User
object has been recovered, you can test using the method assertTrue
whether it matches the one saved earlier. It's as simple as that! 😁
Now let’s run the test! Congratulations, you’ve just created your first test on a database!
For now, you’ve only tested the adding and retrieval of data from the table user, but not the table item. But because I’m so nice, I’ll give you the final tests, so you can test all of the CRUD methods of the item table.
Excerpt from ItemDaoTest
class ItemDaoTest {
...
private val NEW_ITEM_PLACE_TO_VISIT = Item("Visit this dream place !", 0, USER_ID)
private val NEW_ITEM_IDEA = Item("We could do dog sledding ?", 1, USER_ID)
private val NEW_ITEM_RESTAURANTS = Item("This restaurant looks nice", 2, USER_ID)
...
@Test
@Throws(InterruptedException::class)
fun getItemsWhenNoItemInserted() {
// TEST
val items = LiveDataTestUtil.getValue(this.database!!.itemDao().getItems(USER_ID))
assertTrue(items.isEmpty())
}
@Test
@Throws(InterruptedException::class)
fun insertAndGetItems() {
// BEFORE : Adding demo user & demo items
this.database.userDao().createUser(USER_DEMO)
this.database.itemDao().insertItem(NEW_ITEM_PLACE_TO_VISIT)
this.database.itemDao().insertItem(NEW_ITEM_IDEA)
this.database.itemDao().insertItem(NEW_ITEM_RESTAURANTS)
// TEST
val items = LiveDataTestUtil.getValue(this.database.itemDao().getItems(USER_ID))
assertTrue(items.size == 3)
}
@Test
@Throws(InterruptedException::class)
fun insertAndUpdateItem() {
// BEFORE : Adding demo user & demo items. Next, update item added & re-save it
this.database.userDao().createUser(USER_DEMO)
this.database.itemDao().insertItem(NEW_ITEM_PLACE_TO_VISIT)
val itemAdded = LiveDataTestUtil.getValue(this.database.itemDao().getItems(USER_ID))[0]
itemAdded.selected = true
this.database.itemDao().updateItem(itemAdded)
//TEST
val items = LiveDataTestUtil.getValue(this.database.itemDao().getItems(USER_ID))
assertTrue(items.size == 1 && items[0].selected)
}
@Test
@Throws(InterruptedException::class)
fun insertAndDeleteItem() {
// BEFORE : Adding demo user & demo item. Next, get the item added & delete it.
this.database.userDao().createUser(USER_DEMO)
this.database.itemDao().insertItem(NEW_ITEM_PLACE_TO_VISIT)
val itemAdded = LiveDataTestUtil.getValue(this.database.itemDao().getItems(USER_ID))[0]
this.database.itemDao().deleteItem(itemAdded.id)
//TEST
val items = LiveDataTestUtil.getValue(this.database.itemDao().getItems(USER_ID))
assertTrue(items.isEmpty())
}
}
As you can see, we're using all the same methods from our ItemDao
interface, and we’re checking to see if they return the information that they should.
Let's recap!
A rule allows you to define the manner in which your tests will be executed.
Room provides you with a builder called inMemoryDatabaseBuilder, which creates an instance of your database directly in memory.
Beginning in the next part of this course, we will see how to integrate the calls to our database directly from our TodoListActivity controller in as clean a manner as possible, using architecture components. 😉