Android Room Database Migration Instrumented Unit Test (Kotlin)

October 9, 2018

Edit Module build.gradle.

android {
    defaultConfig {
        ...

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }
    }

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

dependencies {
    // existing
    implementation "android.arch.persistence.room:runtime:$android_room_version"
    kapt "android.arch.persistence.room:compiler:$android_room_version"

    // new
    androidTestImplementation "android.arch.persistence.room:testing:$android_room_version"
}

NOTE: Make sure exportSchema = true for @Database, else schema json won’t be generated and later you might bump into FileNotFoundException: Cannot find the schema file in the assets folder.

@Database(entities = [(Pin::class)], version = 1, exportSchema = true)
@TypeConverters(RoomConverters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun pinDao(): PinDao
}

When you compile the app, assets/[DatabaseClassPath] (Android view) or /app/schemas/[DatabaseClassPath] (Project view) is generated with 1.json, 2.json, etc. (the number is the database version).

Perform Android Room database migration.

NOTE: You might want to generate 1.json by performing a compilation first, then only perform the necessary database migration (add new column/table to @Entity and change database version) to generate 2.json.

Create MigrationTest.kt in androidTest.

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    companion object {
        private const val TAG = "MigrationTest"
        private const val TEST_DB = "test-db"
    }

    lateinit var db: SupportSQLiteDatabase

    @Rule @JvmField
    val helper: MigrationTestHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java!!.canonicalName,
            FrameworkSQLiteOpenHelperFactory())

    @Before
    fun setUp() {
        db = helper.createDatabase(TEST_DB, 1)
    }

    @Test
    fun migrate1To2() {
        // db has schema version 1. insert some data using SQL queries.
        // You cannot use DAO classes because they expect the latest schema.
        // db.execSQL(...);
        val values = ContentValues()
        values.put("id", "test01")
        values.put("name", "Test 01")

        val id = db.insert("pin", SQLiteDatabase.CONFLICT_REPLACE, values)

        // Prepare for the next version.
        db.close()

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
        val cursor = db.query("SELECT * FROM pin WHERE rowid = ?", arrayOf(id))

        MatcherAssert.assertThat(cursor, Is(notNullValue()))
        MatcherAssert.assertThat(cursor.moveToFirst(), Is(true))

        val name = cursor.getString("name")
        MatcherAssert.assertThat(name, Is("Test 01"))

        val isLocationAccurate = cursor.getIntOrNull("is_location_accurate")
        // MatcherAssert.assertThat(isLocationAccurate, Is(nullValue()))
        MatcherAssert.assertThat(isLocationAccurate, Is(0))
    }
}

NOTE: Refer to Android Instrumented Unit Test if you are not familiar with Instrumented Unit Test.

NOTE: If you bump into duplicate column name exception, check schema/1.json and schema/2.json to make sure the new column doesn’t exist in schema/1.json.

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.