Testing Migrations

It would be nice if your migrations worked. Users, in particular, appreciate working code… or, perhaps more correctly, get rather angry with non-working code.

Hence, you might want to test the migrations.

This gets a bit tricky, though. The code-generated Room classes are expecting the latest-and-greatest schema version, so you cannot use your DAO for testing older schemas. Besides, RoomDatabase.Builder wants to set up your database with that latest-and-greatest schema automatically.

Fortunately, Room ships with some testing code to help you test your schemas in isolation… though you bypass most of Room to do that.

Adding the Artifact

This testing code is in a separate android.arch.persistence.room:testing artifact, one that you can add via androidTestCompile to put in your instrumentation tests but leave out of your production code:

dependencies {
  implementation "com.android.support:recyclerview-v7:28.0.0"
  implementation 'com.android.support:support-fragment:28.0.0'
  androidTestImplementation 'com.android.support:support-compat:28.0.0'
  androidTestImplementation 'com.android.support:support-core-utils:28.0.0'
  implementation "android.arch.persistence.room:runtime:1.1.1"
  annotationProcessor "android.arch.persistence.room:compiler:1.1.1"
  androidTestImplementation "com.android.support:support-annotations:28.0.0"
  androidTestImplementation 'com.android.support.test:rules:1.0.2'
  androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
}

Adding the Schemas

Remember those exported schemas? While we used them for helping us write the migrations, their primary use is for this testing support code.

By default, those schemas are stored outside of anything that goes into your app. After all, you do not need those JSON files cluttering up your production builds. However, this also means that those schemas are not available to your test code, by default.

However, we can fix that, by adding those schemas to the assets/ used in the androidTest source set, by having this closure in your android closure of your module’s build.gradle file:

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

Here, "$projectDir/schemas".toString() is the same value that we used for the room.schemaLocation annotation processor argument. This snippet tells Gradle to include the contents of that schemas/ directory as part of our assets/.

The result is that our instrumentation test APK will have those directories named after our RoomDatabase classes (e.g., com.commonsware.android.room.TripDatabase/) in the root of assets/. If you have code that uses assets/, make sure that you are taking steps to ignore these extra directories.

Creating and Using a MigrationTestHelper

The testing support comes in the form of a MigrationTestHelper that you can employ in your instrumentation tests.

Adding the Rule

MigrationTestHelper is a JUnit4 rule, which you add to your test case class via the @Rule annotation:

  @Rule
  public MigrationTestHelper helper;

Setting Up the Helper

You then need to create an instance of the MigrationTestHelper, such as in a @Before-annotated method:

  @Before
  public void setUp() {
    helper=new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
      TripDatabase.class.getCanonicalName());
  }

MigrationTestHelper takes two parameters, both of which are a bit unusual.

First, it takes an Instrumentation object. We use those in our test code, but it is rare that we pass them as a parameter. You get your Instrumentation usually by calling getInstrumentation() on the InstrumentationRegistry.

Then, it takes what appears to be the fully-qualified class name of the RoomDatabase whose migrations we wish to test. Technically speaking, this is actually the relative path, inside of assets/, where the schema JSON files are for this particular RoomDatabase. Given the above configuration, each database’s schemas are put into a directory named after the fully-qualified class name of the RoomDatabase, which is why this works. However, if you change the configuration to put the schemas somewhere else in assets/, you would need to modify this parameter to match.

Creating a Database for a Schema Version

There are two main methods on MigrationTestHelper that we will use in testing. One is createDatabase(). This creates the database, as a specific database file, for a specific schema version… including any of our historical ones found in those schema JSON files. Here, we ask the helper to create a database named DB_NAME for schema version 1:

    SupportSQLiteDatabase db=helper.createDatabase(DB_NAME, 1);

As part of testing a migration, you will need to add some sample data to the database, using whatever schema you asked to be used, so that you can confirm that the migration worked as expected and did not wreck the existing data. This code will not be very Room-ish, but more like classic SQLite Android programming:

    SupportSQLiteDatabase db=helper.createDatabase(DB_NAME, 1);

    db.execSQL("INSERT INTO trips (id, title, duration) VALUES (1, NULL, 0)");

    final Cursor firstResults=db.query("SELECT COUNT(*) FROM trips");

    assertEquals(1, firstResults.getCount());
    firstResults.moveToFirst();
    assertEquals(1, firstResults.getInt(0));

    firstResults.close();
    db.close();

Testing a Migration

The other method of note on MigrationTestHelper is runMigrationsAndValidate(). After you have set up a database in its starting conditions via createDatabase() and CRUD operations, runMigrationsAndValidate() will migrate that database from its original schema version to the one that you specify:

    db=helper.runMigrationsAndValidate(DB_NAME, 2, true,
      Migrations.FROM_1_TO_2);

You need to supply the same database name (DB_NAME), a higher schema version (2), and the specific Migration that you want to use (Migration.FROM_1_TO_2).

Not only does this method perform the migration, but it validates the resulting schema against what the entities have set up for that schema version, based on the schema JSON files. If there is something wrong — your migration forgot a newly-added column, for example — your test will fail with an assertion violation. The true parameter shown above determines whether this schema validation will be checked for un-dropped tables. true means that if you have unnecessary tables in the database, the test fails; false means that unnecessary tables are fine and will be ignored.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.