Is everything where it’s supposed to be? Test migrations. (Photo by Dmitri Popov on Unsplash)

Testing Room migrations

In a previous post I explained how database migrations with Room work under the hood. We saw that an incorrect migration implementation can lead to either your app crashing or loss of the user’s data.

On top of this, the SQL statements you execute in the Migration.migrate method are not checked at compile time, opening the door to more issues. Knowing all of this, testing the migrations becomes an essential task. Room provides the MigrationTestHelper test rule which allows you to:

  • Create a database at a given version
  • Run a given set of migrations on a database
  • Validate the database schema

Note that Room will not validate the data in the database. This is something you need to implement yourself.

Here’s what you need to know to test Room migrations.

Under the hood

To test migrations, Room needs to know several things about your current database version: the version number, the entities, the identity hash and the queries for creating and updating the room_master_table. All of these are automatically generated by Room at compile time and stored in a schema JSON file.

In your build.gradle file you specify a folder to place these generated schema JSON files. As you update your schema, you’ll end up with several JSON files, one for every version. Make sure you commit every generated file to source control. The next time you increase your version number again, Room will be able to use the JSON file for testing.

Prerequisites

To enable the generation of the JSON file, update your build.gradle file with the following:

  1. Define the schema location
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}

2. Add the schema location to the source sets

android {

sourceSets {
androidTest.assets.srcDirs +=
files("$projectDir/schemas".toString())
}

3. Add the room testing library to the list of dependencies

dependencies {
androidTestImplementation    
“android.arch.persistence.room:testing:1.0.0-alpha5”
}

Migration test rule

Creating the database, schemas, opening and closing the database, running migrations — that’s a lot of boilerplate code that you would need to write for almost every test. To avoid writing all of this, use the MigrationTestHelper test rule in your migration test class.

@Rule
public MigrationTestHelper testHelper =
new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
<your_database_class>.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());

The MigrationTestHelper heavily relies on the generated JSON file, both for creating the database and for validating the migration.

You are able to create your database in a specific version:

// Create the database with version 2
SupportSQLiteDatabase db =
testHelper.createDatabase(TEST_DB_NAME, 2);

You can run a set of migrations and validate automatically that the schema was updated correctly:

db = testHelper.runMigrationsAndValidate(TEST_DB_NAME, 4, validateDroppedTables, MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4);

Implement tests

The testing strategy is simple:

  1. Open the database in a specific version
  2. Insert some data
  3. Run the migrations and validate the schema
  4. Check that the correct data is in the database.

For example, version 3 of the database adds a new column: date. So, when testing the migration from version 2 to version 3, we check the validity of the data that was inserted in version 2, but also the default value for our new column. Here’s how our AndroidJUnitTest looks like:

@Test
public void migrationFrom2To3_containsCorrectData() throws
IOException {
// Create the database in version 2
SupportSQLiteDatabase db =
testHelper.createDatabase(TEST_DB_NAME, 2);
// Insert some data
insertUser(USER.getId(), USER.getUserName(), db);
//Prepare for the next version
db.close();

// Re-open the database with version 3 and provide MIGRATION_1_2
// and MIGRATION_2_3 as the migration process.
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 3,
validateDroppedTables, MIGRATION_1_2, MIGRATION_2_3);

// MigrationTestHelper automatically verifies the schema
//changes, but not the data validity
// Validate that the data was migrated properly.
User dbUser = getMigratedRoomDatabase().userDao().getUser();
assertEquals(dbUser.getId(), USER.getId());
assertEquals(dbUser.getUserName(), USER.getUserName());
// The date was missing in version 2, so it should be null in
//version 3
assertEquals(dbUser.getDate(), null);
}

Testing the migration from an SQLiteDatabase implementation to Room

I talked before about the steps you need to take to go from a standard SQLiteDatabase implementation to Room, but I didn’t go into details on how to test the migration implementation.

Since the initial database version was not implemented using Room, we don’t have the corresponding JSON file, therefore we cannot use the MigrationTestHelper to create the database.

Here’s what we need to do:

  1. Extend the SQLiteOpenHelper class and, in onCreate, execute the SQL queries that create your database tables.
  2. In the @Before method of your tests, create the database.
  3. In the @After method of your tests, clear the database.
  4. Use your implementation of SQLiteOpenHelper to insert the data needed for the tests checking the migration from your SQLiteDatabase version to other versions that use Room.
  5. Use MigrationTestHelper to run the migration and validate the schema.
  6. Check the validity of the database data.

Database version 1 was implemented using SQLiteDatabase API, then in version 2 we migrated to Room and, in version 3 we added a new column. A test checking a migration from version 1 to 3 looks like this:

@Test
public void migrationFrom1To3_containsCorrectData() throws IOException {
// Create the database with the initial version 1 schema and
//insert a user
SqliteDatabaseTestHelper.insertUser(1, USER.getUserName(), sqliteTestDbHelper);

// Re-open the database with version 3 and provide MIGRATION_1_2
// and MIGRATION_2_3 as the migration process.
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 3, true,
MIGRATION_1_2, MIGRATION_2_3);

// Get the latest, migrated, version of the database
// Check that the correct data is in the database
User dbUser = getMigratedRoomDatabase().userDao().getUser();
assertEquals(dbUser.getId(), 1);
assertEquals(dbUser.getUserName(), USER.getUserName());
// The date was missing in version 2, so it should be null in
//version 3
assertEquals(dbUser.getDate(), null);
}

Show me the code

You can check out the implementation in this sample app. To ease the comparison every database version was implemented in its own flavor:

  1. sqlite — Uses SQLiteOpenHelper and traditional SQLite interfaces.
  2. room — Replaces implementation with Room and provides migration to version 2
  3. room2 — Updates the DB to a new schema, version 3
  4. room3 — Updates the DB to a new, version 4. Provides migration paths to go from version 2 to 3, version 3 to 4 and version 1 to 4.

Conclusion

With Room, implementing and testing migrations is easy. The MigrationTestHelper test rule allows you to to open your database at any version, to run migrations and validate schemas in only a few lines of code.

Have you started using Room and implemented migrations? If so, let us know how it went in the comments below.