Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test Realm migrations?

Tags:

android

realm

Im trying to unit test a migration on Realm. My main question is: how can I maintain different schema versions of a RealmObject so as to be able to create a an instance of the old object, do the migration and then check if it is correct according the new schema version?

I started by trying to keep the different schema versions but it wont compile since the objects have the same name, despite being on different packages.

like image 655
Fábio Carballo Avatar asked Dec 19 '22 15:12

Fábio Carballo


2 Answers

At Realm we test the migration mechanism by storing old Realm files as assets (see https://github.com/realm/realm-java/tree/master/realm/realm-library/src/androidTest/assets) and then write tests to check the result after a migration test (see https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/RealmMigrationTests.java).

like image 83
geisshirt Avatar answered Dec 29 '22 10:12

geisshirt


With some help from the Realm source code as well as this project I figured out how to perform Realm migration unit tests.

Step by step, this is how you construct a test:

  • Create a Realm instance from a certain state. A certain schema version in the past (your old schema you want to migrate)
  • Create a RealmConfiguration as follows: (1) Upgrade schema version to the latest for your project (2) keep the same name as the Realm instance above (3) add the migration code that will run the migration.
  • Create a Realm instance from this RealmConfiguration created above. If the migration runs successfully, no exception will be thrown. You will have successfully migrated your code from that state to the current app's Realm schema version.

Let's say your app was previously running on schema version 0 when you first began building your app. Now, you are on schema version 1 with some changes you made. You are writing tests to see if you can migrate successfully from version 0 to version 1.

Create a Realm file of your Realm database at version 0. What I usually do to do this is to checkout my code using a git commit I made when my app was using version 0, run my app to create the Realm database, then using the bash script I mention in this post I copy my Realm file to my computer file system. Store this realm file in your androidTest/assets assets directory in your app project.

Copy over the TestRealmConfigurationFactory file into your project. This is what you will use to create a Realm instance from your androidTest/assets Realm files.

Create the test. Here is some sample code I made from migration tests I run:

@RunWith(AndroidJUnit4::class)
open class MigrationTest {

    @get:Rule open val configFactory = TestRealmConfigurationFactory()
    @get:Rule open val thrown = ExpectedException.none()

    private lateinit var context: Context

    @Before
    fun setup() {
        context = InstrumentationRegistry.getInstrumentation().context
    }

    @Test(expected = RealmMigrationNeededException::class)
    @Throws(Exception::class)
    fun migrate_migrationNeededIsThrown() {
        val REALM_NAME = "0.realm"
        val realmConfig = RealmConfiguration.Builder()
                .name(REALM_NAME)
                .schemaVersion(0)
                .build()
        configFactory.copyRealmFromAssets(context, REALM_NAME, realmConfig)

        // should fail because my code base realm models have changed *since* this 0.realm file.
        // When you want to get a realm instance, it will take what realm objects are already in memory (mapped by the "name" property of the RealmConfiguration) and it will compare it to the models in the application. If they are different, a realm migration exception will be thrown. So, you need to make sure to add Realm migrations to your code at all times.
        val realm = Realm.getInstance(realmConfig)
        realm.close()
    }

    @Test fun migrate_migrateFrom0toLatest() {
        val REALM_NAME = "0.realm"
        val realmConfig = RealmConfiguration.Builder()
                .name(REALM_NAME)
                .schemaVersion(RealmInstanceManager.schemaVersion)
                .migration { dynamicRealm, oldVersion, newVersion ->
                    val schema = dynamicRealm.schema

                    for (i in oldVersion until newVersion) {
                        RealmInstanceManager.migrations[i.toInt()].runMigration(schema)
                    }
                }
                .build()
        configFactory.copyRealmFromAssets(context, REALM_NAME, realmConfig)

        val realm = Realm.getInstance(realmConfig)
        realm.close()
    }

    // convenient method to generate 1 realm file in app directory to be able to copy to assets directory for the next migration test when schema version changes.
    @Test fun createFileForCurrentVersionToCopyToAssetsFile() {
        val REALM_NAME = "${RealmInstanceManager.schemaVersion}.realm"
        val realmConfig = RealmConfiguration.Builder()
                .name(REALM_NAME)
                .schemaVersion(RealmInstanceManager.schemaVersion)
                .build()

        Realm.deleteRealm(realmConfig)
        val realm = Realm.getInstance(realmConfig)
        realm.close()
    }

}

For reference, here is my RealmInstanceManager and other companion files I created to make my migrations isolated.

open class RealmInstanceManager(private val userManager: UserManager) {

    companion object {
        val migrations: List<RealmSchemaMigration> = listOf(
                Migration1()
        )

        var schemaVersion: Long = 0L
            get() = migrations.size.toLong()
    }

}

My Migration1 class:

class Migration1: RealmSchemaMigration {

    override fun runMigration(schema: RealmSchema) {
        schema.get(OwnerModel::class.java.simpleName)!!
                .addField("avatar_url", String::class.java)
                .setRequired("avatar_url", true)
                .transform { it.set("avatar_url", "") }
    }

}

And lastly, my RealmSchemaMigration interface:

interface RealmSchemaMigration {

    fun runMigration(schema: RealmSchema)

}

I have found the above configuration of files and interfaces a nice way to manage my migration files in my project to run for tests as well as the app itself.

Really, you're done. The migration test is in the function migrate_migrateFrom0toLatest() where I take my Realm file asset that is from schema version 0 (named "0.realm") and I migrate it to my latest version in my codebase. The function createFileForCurrentVersionToCopyToAssetsFile() is not required but I like having it because after I run my tests I can copy this newly created Realm file of mine on my device into my assets directory to use for the next migration test that I run. Much handier then having to do the git checkout method I explained above.

like image 39
levibostian Avatar answered Dec 29 '22 10:12

levibostian