Room DB: Advanced Data Persistence

This tutorial introduces more advanced concepts for use with the Room persistence library, such as migration and indexing. By Lance Gleason.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Testing Your Migrations

Now that you have the code needed to update your database, you’ll want to test your migration. To do that, you could add some code and Log statements to your activity. But a better way is to write an Espresso test.

To create an Espresso test, right-click on the (androidTest) version of your listmaster package. Select New ▸ Kotlin File/Class. Name it ListItemMigrationTest and press OK:

New Kotlin file

When the file opens, add in the following:

@RunWith(AndroidJUnit4::class)
class ListItemMigrationTest {

  private val TEST_DB_NAME = "migration_test"

  private lateinit var database: SupportSQLiteDatabase

  //1
  @Rule
  @JvmField
  val migrationTestHelperRule = MigrationTestHelper(
      InstrumentationRegistry.getInstrumentation(),
      "com.raywenderlich.listmaster.AppDatabase",
      FrameworkSQLiteOpenHelperFactory())

  //2
  @Before
  fun setup(){
    database = migrationTestHelperRule.createDatabase(TEST_DB_NAME, 1)
    database.execSQL("INSERT INTO list_categories (id, category_name) VALUES" +
        " (1, 'Purr Programming Supplies'), (2, 'Canine Coding Supplies')")
  }

  //3
  @After
  fun teardown(){
    database.execSQL("DROP TABLE IF EXISTS list_categories")
    database.execSQL("DROP TABLE IF EXISTS list_items")
    database.close()
  }
}

The code in the test class has the following capabilities:

  1. Creates a rule that initializes an Espresso test with the AppDatabase.
  2. Creates a version 1 instance of the database with data and runs the migration before every test.
  3. Removes all tables from the database after each test.

In the test setup, SQL statements are used to insert test data into version 1 of the database. This is because the new DAOs for version 2 are not available until all migrations have been executed.

As part of testing your migration, you need to get a version of the database that has already been migrated. To make your test more readable, paste the following helper method into your ListItemMigrationTest class.

private fun getMigratedRoomDatabase(): AppDatabase {
  //1
  val appDatabase = Room.databaseBuilder(
      InstrumentationRegistry.getTargetContext(),
      AppDatabase::class.java, TEST_DB_NAME)
      //2
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      //3
      .build()
  //4
  migrationTestHelperRule.closeWhenFinished(appDatabase)
  return appDatabase
}

Breaking down the parts of this method:

  1. Call the Room database builder, passing in the name of your test database.
  2. Add your migration to the builder.
  3. Build the database.
  4. Tell the test rule to close the database when finished.

LiveData Espresso Testing

When you are testing code that runs in another thread, a common problem is understanding how to get your test to wait for the threads to finish before doing an assert on the result.

In the case of LiveData, a query normally runs on a background thread, and you attach an observer to process the retrieved values. To get around this in tests, the projects includes a small Kotlin extension called blockingObserve in TestExtensions.kt. This file is under the root listmaster package in the (androidTest) section of the project.

Open it up and you will see the following:

fun <T> LiveData<T>.blockingObserve(): T? {
  var value: T? = null
  val latch = CountDownLatch(1)
  val innerObserver = Observer<T> {
    value = it
    latch.countDown()
  }
  observeForever(innerObserver)
  latch.await(2, TimeUnit.SECONDS)
  return value
}

It adds an observer to the LiveData object and blocks until a value is returned so that your test does not finish executing before the values are returned from the database.

Note: If this is your first time working with extensions, the Kotlin documentation is a great place to start for understanding how they work.

Note: If this is your first time working with extensions, the Kotlin documentation is a great place to start for understanding how they work.

Now that you’ve built up the scaffolding of your test, it’s time to create a test to validate that the migration works. Paste the following method into ListItemMigrationTest:

@Test
fun migrating_from_1_to_2_retains_version_1_data() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  assertEquals(2, listCategories!!.size)
  assertEquals("Purr Programming Supplies",
      listCategories.first().categoryName)
  assertEquals(1, listCategories.first().id)
  assertEquals("Canine Coding Supplies",
      listCategories.last().categoryName)
  assertEquals(2, listCategories.last().id)
}

Reading the assertEquals statements, you will see verifications for the following things in the list_categories table using its DAO:

  • There are total of two records.
  • The first record is Purr Programming Supplies with an ID of 1.
  • The second record is Canine Coding Supplies with an ID of 2.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run:

Run the tests

You’ll need to select a device or emulator to run the Espresso tests on.

Verify that the result is “green” (passing):

Passing tests

Next, look in your assets directory and you will see a schema file for version 2 of your database called 2.json:
Schema version 2 file

Note: You should version these files in your project so that you can test your migrations as you increase the versions.

Note: You should version these files in your project so that you can test your migrations as you increase the versions.

Now that you are in the testing groove, you are going to test inserting a record into the new table while referencing an existing category. To do this, paste the following method into your ListItemMigrationTest class:

@Test
fun inserting_a_record_into_list_items_after_migrating_from_1_to_2_succeeds() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  // insert a record in the new table
  val listItemDao = getMigratedRoomDatabase().listItemDao()
  val purrProgrammingListItem = ListItem("desk cushion", 1,
      listCategories!!.first().id)
  listItemDao.insertAll(purrProgrammingListItem)

  // validate that a record can be added to the new table
  val purrProgrammingList = listItemDao.getAll().blockingObserve()
  assertEquals(1, purrProgrammingList!!.size)
  val firstPurrProgrammingItem = purrProgrammingList.first()
  assertEquals("desk cushion", firstPurrProgrammingItem.itemDescription)
  assertEquals(1, firstPurrProgrammingItem.itemPriority)
  assertEquals(listCategories.first().id,
      firstPurrProgrammingItem.listCategoryId)
  assertEquals(1, firstPurrProgrammingItem.id)
}

This test does the following using the entity DAOs:

  • Inserts a record with a priority of 1 and item description of desk cushion associated with the Purr Programming Supplies category.
  • Checks that only one record was actually inserted.
  • Verifies that the persisted values match what we’ve added.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run. All tests should be green.

Migrating When Your App Initializes

Great! You’re confident that your migration code works. Now, you need to run this migration in the app. Since the goal is to have your app update the database, you’re going to need it to execute it when the user first opens the app. To do that, open the ListMasterApplication class and replace the onCreate() method with the following:

override fun onCreate() {
  super.onCreate()
  ListMasterApplication.database = Room.databaseBuilder(
      this,
      AppDatabase::class.java,
      "list-master-db")
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      .build()
}

The addMigrations() call does the following:

  • Checks if the migration has been applied to the database. If not, it runs the migration.
  • If migration has already been applied to the database, it will do nothing.