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:
- 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:
- Open the database in a specific version
- Insert some data
- Run the migrations and validate the schema
- 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:
- Extend the
SQLiteOpenHelper
class and, inonCreate
, execute the SQL queries that create your database tables. - In the
@Before
method of your tests, create the database. - In the
@After
method of your tests, clear the database. - Use your implementation of
SQLiteOpenHelper
to insert the data needed for the tests checking the migration from yourSQLiteDatabase
version to other versions that use Room. - Use
MigrationTestHelper
to run the migration and validate the schema. - 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:
- sqlite — Uses
SQLiteOpenHelper
and traditional SQLite interfaces. - room — Replaces implementation with Room and provides migration to version 2
- room2 — Updates the DB to a new schema, version 3
- 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.