loading...

Testing Realm Migrations.

aniketsmk profile image Aniket Kadam Originally published at dev.to on ・7 min read

Why test?

Realm migrations are often, left for the end. They can be stressful, and if you’re doing a manual test, it can take really long to set up or even create the conditions to test.

Let’s see how we can take it down from 20 minutes of manual labor, to 20 seconds of automated test.

How?

The idea is simple,

  • Store the older copy of the db that you will migrate.
  • In your instrumentation test, copy the saved db to a temp location.
  • Set your configuration to the new schema number and get an instance. (this is where it’s most likely to crash on the spot)
  • Verify that you got the right structure.

Let’s begin with an example.

Full Source Code available here https://github.com/AniketSK/RealmMigrationTestsExample

I have this class. Let’s call it version 1.

public class Dog extends RealmObject {
    String name;
}

I want to keep track of my dogs’ age, so I want to change it to this: Which we will make version 2.

public class Dog extends RealmObject {
    String name;
    int age;
}

This is what my configuration looked like for v1.

Realm.init(this);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder()
        .schemaVersion(1)
        .build();
Realm.setDefaultConfiguration(realmConfiguration);
Realm realm = Realm.getDefaultInstance();
realm.close();

The only two lines I will change here for v2 will be in the configuration:

RealmConfiguration realmConfiguration = new RealmConfiguration.Builder()
        .schemaVersion(2)
        .migration(new MigrationExample())
        .build();

Straightforward, I update the schema version and provide a migration class. This looks like:

class MigrationExample implements RealmMigration {

    @Override
    public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {
        if (oldVersion < 2) {
            updateToVersion2(realm.getSchema());
        }
    }

    private void updateToVersion2(RealmSchema schema) {
        RealmObjectSchema dogSchema = schema.create(Dog.class.getSimpleName()); // Get the schema of the class to modify.
        dogSchema.addField("age", int.class); // Add the new field.
    }
}

There’s a problem here i’ve left in, but we’ll discover what it is during the test :) Can you see it already?

If you’re in a rush, here’s the finished tests, be sure to grab the support file from Realm below it.

Understand

Now, that your code is tested and deployed to production you can relax and understand how this works.

Let’s begin with setting up for the test.

  • Store the older copy of the db that you will migrate.

For this, you’re best off running your app on an emulator.

Why?

Answer: Emulators provide root access by default and it’s the easiest way to get the older copy of the realm. The older copy of realm is by default in the internal storage of your app in the files folder.

Install and run your older version of the app, on the emulator. This will likely initialize your realm and create the realm file in internal storage. Now you will be able to pull it.

Easiest is with a terminal command with adb.

adb pull /data/data/com.aniket.realmmigrationtestexample/files/default.realm ~/RealmMigrationTest/app/src/androidTest/assets/realmdb_1.realm

With this two things are achieved:

  1. The realm file for your app is pulled from internal phone (emulator) storage to your computer.
  2. It is stored in the assets of the androidTest folder in your app’s project, with a new name! Now it is called realmdb_1.realm

Great, we’re done with step 1!

Note: If you needed to do more complex things, such as create the entire state of your app as it would be after a lot of user interaction, it’s easiest to just do that once manually and then pull it. Be sure all the data you wanted is actually written to disk!

Setting up the test

  • In your instrumentation test, copy the saved db to a temp location.

This line jumps a few steps. How do we copy the file we have in assets to a new realm file that we can use?

Realm already has written a class that helps with this and a lot of support activities required for it. This is the TestRealmConfigurationFactory, which can be taken from their repo from here https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/RealmMigrationTests.java

Why copy their file? Surely there’s a better way?

Answer: This is internal to their project and just isn’t exported elsewhere in a manner that I’m aware of. If you do know of a better way to get this into your project without copying it, let me know in the comments.

Ok, now that you have this file, let’s create a bog standard instrumentation test to use it.

@RunWith(AndroidJUnit4.class)
public class MigrationExampleTest {

    @Rule
    public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory();

    @Before
    public void setUp() throws Exception {

    }

    @Test
    public void migrate() throws Exception {

    }

}

Your migration test should look like this to start with.

Note, if you don’t have the TestRealmConfigurationFactory copied to your project yet, it will show up as an error.

If you missed it, you’ll need to pick this up from the earlier mentioned location. Be sure to change the package and keep the copyright text!

Setup Complete!

Writing the tests

Now let’s begin with the tests themselves. Add this to the tests.

@Test(expected = RealmMigrationNeededException.class)
public void migrate_migrationNeededIsThrown() throws Exception {
    String REALM_NAME = "realmdb_1.realm";
    RealmConfiguration realmConfig = new RealmConfiguration.Builder()
            .name(REALM_NAME) // Keep using the temp realm.
            .schemaVersion(1) // Original realm version.
            .build();// Get a configuration instance.
    configFactory.copyRealmFromAssets(context, REALM_NAME, realmConfig); // Copy the stored version 1 realm file from assets to a NEW location.
    Realm.getInstance(realmConfig);
}

The REALM_NAME, should match the name of the file you pulled from adb. Recall that we renamed it from its default name to realmdb_1.realm and stored it in the assets folder, for androidTest. Not the assets folder under main!

Next we set the schema version as it was originally, and get a realm config.

Then with the configFactory, copy the realm in the assets, to a new location separate from the default realm. This just prevents us from deleting the default realm (since the TestRealmConfigFactory deletes the namedRealm before it’s copied)

Then we try to get an instance of it. Which should fail since the Dog object has already had the age field added to it.

This verifies that a MigrationNeededException is actually thrown.

Test Part 2:

In this test, we’re going actually do the migration!

Get the realm configuration again, this time incrementing the schema version and adding your main Migration class.

@Test
public void migrate_migrationSuceeds() throws Exception {
    String REALM_NAME = "realmdb_1.realm"; // Same name as the file for the old realm which was copied to assets.
    RealmConfiguration realmConfig = new RealmConfiguration.Builder()
            .name(REALM_NAME)
            .schemaVersion(2) // NEW realm version.
            .migration(new MigrationExample())
            .build();// Get a configuration instance.
    configFactory.copyRealmFromAssets(context, REALM_NAME, realmConfig); // Copy the stored version 1 realm file
    // from assets to a NEW location.
    // Note: the old file is always deleted for you.
    // by the copyRealmFromAssets.
    Realm realm = Realm.getInstance(realmConfig);
    assertTrue("The age field was not added.", realm.getSchema().get(Dog.class.getSimpleName())
        .hasField("age"));

    assertEquals(realm.getSchema().get(Dog.class.getSimpleName())
        .getFieldType("age"), RealmFieldType.INTEGER);
    realm.close();
}

Once this is done, get the realm instance again. This time the migration will run, and your test should fail!

Here we finally run into the error I mentioned I’d put in. I originally created the Dog.class all over again before adding the field.

So in the MigrationExample.java change ‘create’:

private void updateToVersion2(RealmSchema schema) {
    RealmObjectSchema dogSchema = schema.create(Dog.class.getSimpleName()); // Get the schema of the class to modify.
    dogSchema.addField("age", int.class); // Add the new field.
}

To ‘get’:

private void updateToVersion2(RealmSchema schema) {
    RealmObjectSchema dogSchema = schema.get(Dog.class.getSimpleName()); // Get the schema of the class to modify.
    dogSchema.addField("age", int.class); // Add the new field.
}

Run this again and your test will pass!

Congratulations, you can now test Realm Migrations!

Conclusion:

Testing realm migrations with instrumentation tests is pretty easy and can help you track down tricky bugs. For instance, you can now run realm queries and other operations on the final realm you receive at the end of this test. You can verify for instance, that all your existing dogs, had a default age assigned to them. You could ensure that some data transformation you were doing, completed successfully.

As always, you can set debug pointers and step through the migrations to fix issues you encounter.

I hope this stripped down example will help you to more thoroughly test your application and bring a difficult to set test, into the realm of easy testability.

See more examples of how to use this at:

realm/realm-java

Until next time, remember, if you’re doing any manual repetitive work, there’s always a better way!

You can reach me on twitter @aniketsmk I’m always happy to get comments and suggestions.

Notes:

If you find emulators difficult to run, or your test db can only be created on a real device, you can always use

File externalStorageFile = new File(getExternalFilesDir(null), "copiedInternalRealmToExternal.realm");
realm.writeCopyTo(externalStorageFile);

and pick up the file from there. This will copy your internal realm to an external location, where even real devices wouldn’t obstruct you from picking it up.

This was edited on 7th August, 2017 to correct one of the tests.

Discussion

pic
Editor guide