Issue#3 Implement migration test which runs both on Real device and via Robolectric
This commit is contained in:
parent
4a1254d092
commit
2aca350175
16 changed files with 1028 additions and 2 deletions
|
|
@ -54,17 +54,21 @@ android {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest {
|
androidTest {
|
||||||
java.srcDirs += "src/sharedTest/java"
|
java.srcDirs += "src/sharedTest/java"
|
||||||
|
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
androidTestHilt {
|
androidTestHilt {
|
||||||
java.srcDirs += "src/sharedTestHilt/java"
|
java.srcDirs += "src/sharedTestHilt/java"
|
||||||
|
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
androidTestKoin {
|
androidTestKoin {
|
||||||
java.srcDirs += "src/sharedTestKoin/java"
|
java.srcDirs += "src/sharedTestKoin/java"
|
||||||
|
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
java.srcDirs += "src/sharedTest/java"
|
java.srcDirs += "src/sharedTest/java"
|
||||||
java.srcDirs += "src/robolectricTest/java"
|
java.srcDirs += "src/robolectricTest/java"
|
||||||
|
resources.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
testHilt {
|
testHilt {
|
||||||
java.srcDirs += "src/sharedTestHilt/java"
|
java.srcDirs += "src/sharedTestHilt/java"
|
||||||
|
|
@ -149,6 +153,7 @@ dependencies {
|
||||||
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
|
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
|
||||||
kaptTest "com.google.dagger:hilt-compiler:$hilt_version"
|
kaptTest "com.google.dagger:hilt-compiler:$hilt_version"
|
||||||
|
|
||||||
|
androidTestImplementation "androidx.room:room-testing:$androidx_room_version"
|
||||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
||||||
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version"
|
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version"
|
||||||
androidTestImplementation "junit:junit:$testing_junit4_version"
|
androidTestImplementation "junit:junit:$testing_junit4_version"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "3723fe73a9d3dc43de8ff3e52ec46490",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "FavouriteEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`content_id` TEXT NOT NULL, PRIMARY KEY(`content_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "contentId",
|
||||||
|
"columnName": "content_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"content_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3723fe73a9d3dc43de8ff3e52ec46490')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import org.junit.runners.model.Statement
|
||||||
|
|
||||||
|
class AndroidMigrationTestRule : SharedMigrationTestRule {
|
||||||
|
|
||||||
|
private val migrationTestHelper: MigrationTestHelper
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>
|
||||||
|
) {
|
||||||
|
migrationTestHelper = MigrationTestHelper(instrumentation, databaseClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>
|
||||||
|
) {
|
||||||
|
migrationTestHelper = MigrationTestHelper(instrumentation, databaseClass, specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>,
|
||||||
|
openFactory: SupportSQLiteOpenHelper.Factory
|
||||||
|
) {
|
||||||
|
migrationTestHelper =
|
||||||
|
MigrationTestHelper(instrumentation, databaseClass, specs, openFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apply(base: Statement, description: Description): Statement =
|
||||||
|
migrationTestHelper.apply(base, description)
|
||||||
|
|
||||||
|
override fun closeWhenFinished(db: RoomDatabase) =
|
||||||
|
migrationTestHelper.closeWhenFinished(db)
|
||||||
|
|
||||||
|
override fun closeWhenFinished(db: SupportSQLiteDatabase) =
|
||||||
|
migrationTestHelper.closeWhenFinished(db)
|
||||||
|
|
||||||
|
override fun createDatabase(name: String, version: Int): SupportSQLiteDatabase =
|
||||||
|
migrationTestHelper.createDatabase(name, version)
|
||||||
|
|
||||||
|
override fun runMigrationsAndValidate(
|
||||||
|
name: String,
|
||||||
|
version: Int,
|
||||||
|
validateDroppedTables: Boolean,
|
||||||
|
vararg migrations: Migration
|
||||||
|
): SupportSQLiteDatabase =
|
||||||
|
migrationTestHelper.runMigrationsAndValidate(
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
validateDroppedTables,
|
||||||
|
*migrations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
|
||||||
|
object AndroidMigrationTestRuleFactory : SharedMigrationTestRuleFactory {
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
AndroidMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
AndroidMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass,
|
||||||
|
specs
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>,
|
||||||
|
openFactory: SupportSQLiteOpenHelper.Factory
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
AndroidMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass,
|
||||||
|
specs,
|
||||||
|
openFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,7 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
|
||||||
|
|
||||||
override fun createSnackbarVerification(): SnackbarVerificationTestRule =
|
override fun createSnackbarVerification(): SnackbarVerificationTestRule =
|
||||||
AndroidTestSnackbarVerificationTestRule
|
AndroidTestSnackbarVerificationTestRule
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
|
||||||
|
AndroidMigrationTestRuleFactory
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import org.fnives.test.showcase.storage.favourite.FavouriteEntity
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [FavouriteEntity::class],
|
entities = [FavouriteEntity::class],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class LocalDatabase : RoomDatabase() {
|
abstract class LocalDatabase : RoomDatabase() {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package org.fnives.test.showcase.storage.database
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import org.fnives.test.showcase.storage.LocalDatabase
|
import org.fnives.test.showcase.storage.LocalDatabase
|
||||||
|
import org.fnives.test.showcase.storage.migation.Migration1To2
|
||||||
|
|
||||||
object DatabaseInitialization {
|
object DatabaseInitialization {
|
||||||
|
|
||||||
fun create(context: Context): LocalDatabase =
|
fun create(context: Context): LocalDatabase =
|
||||||
Room.databaseBuilder(context, LocalDatabase::class.java, "local_database")
|
Room.databaseBuilder(context, LocalDatabase::class.java, "local_database")
|
||||||
|
.addMigrations(Migration1To2())
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package org.fnives.test.showcase.storage.favourite
|
package org.fnives.test.showcase.storage.favourite
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class FavouriteEntity(@PrimaryKey val contentId: String)
|
data class FavouriteEntity(
|
||||||
|
@ColumnInfo(name = "content_id")
|
||||||
|
@PrimaryKey val contentId: String
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.fnives.test.showcase.storage.migation
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
class Migration1To2 : Migration(1, 2) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE FavouriteEntity RENAME TO FavouriteEntityOld")
|
||||||
|
database.execSQL("CREATE TABLE FavouriteEntity(content_id TEXT NOT NULL PRIMARY KEY)")
|
||||||
|
database.execSQL("INSERT INTO FavouriteEntity(content_id) SELECT contentId FROM FavouriteEntityOld")
|
||||||
|
database.execSQL("DROP TABLE FavouriteEntityOld")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,632 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Instrumentation;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.arch.core.executor.ArchTaskExecutor;
|
||||||
|
import androidx.room.AutoMigration;
|
||||||
|
import androidx.room.DatabaseConfiguration;
|
||||||
|
import androidx.room.Room;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
import androidx.room.RoomOpenHelper;
|
||||||
|
import androidx.room.RoomOpenHelper.ValidationResult;
|
||||||
|
import androidx.room.migration.AutoMigrationSpec;
|
||||||
|
import androidx.room.migration.Migration;
|
||||||
|
import androidx.room.migration.bundle.DatabaseBundle;
|
||||||
|
import androidx.room.migration.bundle.DatabaseViewBundle;
|
||||||
|
import androidx.room.migration.bundle.EntityBundle;
|
||||||
|
import androidx.room.migration.bundle.FieldBundle;
|
||||||
|
import androidx.room.migration.bundle.ForeignKeyBundle;
|
||||||
|
import androidx.room.migration.bundle.FtsEntityBundle;
|
||||||
|
import androidx.room.migration.bundle.IndexBundle;
|
||||||
|
import androidx.room.migration.bundle.SchemaBundle;
|
||||||
|
import androidx.room.util.FtsTableInfo;
|
||||||
|
import androidx.room.util.TableInfo;
|
||||||
|
import androidx.room.util.ViewInfo;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.rules.TestWatcher;
|
||||||
|
import org.junit.runner.Description;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of {@link androidx.room.testing.MigrationTestHelper MigrationTestHelper} except it uses resources folder instead of assets.
|
||||||
|
*
|
||||||
|
* This is needed since in unit tests we cannot add to the asset folder easily, without polluting the apk.
|
||||||
|
*
|
||||||
|
* reference: https://github.com/robolectric/robolectric/issues/2065
|
||||||
|
*/
|
||||||
|
public class RobolectricMigrationTestHelper extends TestWatcher implements SharedMigrationTestRule {
|
||||||
|
private static final String TAG = "RobolectricMigrationTestHelper";
|
||||||
|
private final String mAssetsFolder;
|
||||||
|
private final SupportSQLiteOpenHelper.Factory mOpenFactory;
|
||||||
|
private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
|
||||||
|
private List<WeakReference<RoomDatabase>> mManagedRoomDatabases = new ArrayList<>();
|
||||||
|
private boolean mTestStarted;
|
||||||
|
private Instrumentation mInstrumentation;
|
||||||
|
@Nullable
|
||||||
|
private List<AutoMigrationSpec> mSpecs;
|
||||||
|
@Nullable
|
||||||
|
private Class<? extends RoomDatabase> mDatabaseClass;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new migration helper. It uses the Instrumentation context to load the schema
|
||||||
|
* (falls back to the app resources) and the target context to create the database.
|
||||||
|
*
|
||||||
|
* @param instrumentation The instrumentation instance.
|
||||||
|
* @param databaseClass The Database class to be tested.
|
||||||
|
*/
|
||||||
|
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation,
|
||||||
|
@NonNull Class<? extends RoomDatabase> databaseClass) {
|
||||||
|
this(instrumentation, databaseClass, new ArrayList<>(),
|
||||||
|
new FrameworkSQLiteOpenHelperFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new migration helper. It uses the Instrumentation context to load the schema
|
||||||
|
* (falls back to the app resources) and the target context to create the database.
|
||||||
|
* <p>
|
||||||
|
* An instance of a class annotated with {@link androidx.room.ProvidedAutoMigrationSpec} has
|
||||||
|
* to be provided to Room using this constructor. MigrationTestHelper will map auto migration
|
||||||
|
* spec classes to their provided instances before running and validatingt the Migrations.
|
||||||
|
*
|
||||||
|
* @param instrumentation The instrumentation instance.
|
||||||
|
* @param databaseClass The Database class to be tested.
|
||||||
|
* @param specs The list of available auto migration specs that will be provided to
|
||||||
|
* Room at runtime.
|
||||||
|
*/
|
||||||
|
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation,
|
||||||
|
@NonNull Class<? extends RoomDatabase> databaseClass,
|
||||||
|
@NonNull List<AutoMigrationSpec> specs) {
|
||||||
|
this(instrumentation, databaseClass, specs, new FrameworkSQLiteOpenHelperFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new migration helper. It uses the Instrumentation context to load the schema
|
||||||
|
* (falls back to the app resources) and the target context to create the database.
|
||||||
|
* <p>
|
||||||
|
* An instance of a class annotated with {@link androidx.room.ProvidedAutoMigrationSpec} has
|
||||||
|
* to be provided to Room using this constructor. MigrationTestHelper will map auto migration
|
||||||
|
* spec classes to their provided instances before running and validatingt the Migrations.
|
||||||
|
*
|
||||||
|
* @param instrumentation The instrumentation instance.
|
||||||
|
* @param databaseClass The Database class to be tested.
|
||||||
|
* @param specs The list of available auto migration specs that will be provided to
|
||||||
|
* Room at runtime.
|
||||||
|
* @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
|
||||||
|
*/
|
||||||
|
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation,
|
||||||
|
@NonNull Class<? extends RoomDatabase> databaseClass,
|
||||||
|
@NonNull List<AutoMigrationSpec> specs,
|
||||||
|
@NonNull SupportSQLiteOpenHelper.Factory openFactory
|
||||||
|
) {
|
||||||
|
String assetsFolder = databaseClass.getCanonicalName();
|
||||||
|
mInstrumentation = instrumentation;
|
||||||
|
if (assetsFolder.endsWith("/")) {
|
||||||
|
assetsFolder = assetsFolder.substring(0, assetsFolder.length() - 1);
|
||||||
|
}
|
||||||
|
mAssetsFolder = assetsFolder;
|
||||||
|
mOpenFactory = openFactory;
|
||||||
|
mDatabaseClass = databaseClass;
|
||||||
|
mSpecs = specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void starting(Description description) {
|
||||||
|
super.starting(description);
|
||||||
|
mTestStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the database in the given version.
|
||||||
|
* If the database file already exists, it tries to delete it first. If delete fails, throws
|
||||||
|
* an exception.
|
||||||
|
*
|
||||||
|
* @param name The name of the database.
|
||||||
|
* @param version The version in which the database should be created.
|
||||||
|
* @return A database connection which has the schema in the requested version.
|
||||||
|
* @throws IOException If it cannot find the schema description in the assets folder.
|
||||||
|
*/
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public SupportSQLiteDatabase createDatabase(@NotNull String name, int version) throws IOException {
|
||||||
|
File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
|
||||||
|
if (dbPath.exists()) {
|
||||||
|
Log.d(TAG, "deleting database file " + name);
|
||||||
|
if (!dbPath.delete()) {
|
||||||
|
throw new IllegalStateException("There is a database file and I could not delete"
|
||||||
|
+ " it. Make sure you don't have any open connections to that database"
|
||||||
|
+ " before calling this method.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SchemaBundle schemaBundle = loadSchema(version);
|
||||||
|
RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
|
||||||
|
DatabaseConfiguration configuration = new DatabaseConfiguration(
|
||||||
|
mInstrumentation.getTargetContext(),
|
||||||
|
name,
|
||||||
|
mOpenFactory,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
RoomDatabase.JournalMode.TRUNCATE,
|
||||||
|
ArchTaskExecutor.getIOThreadExecutor(),
|
||||||
|
ArchTaskExecutor.getIOThreadExecutor(),
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Collections.<Integer>emptySet(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
|
||||||
|
new CreatingDelegate(schemaBundle.getDatabase()),
|
||||||
|
schemaBundle.getDatabase().getIdentityHash(),
|
||||||
|
// we pass the same hash twice since an old schema does not necessarily have
|
||||||
|
// a legacy hash and we would not even persist it.
|
||||||
|
schemaBundle.getDatabase().getIdentityHash());
|
||||||
|
return openDatabase(name, roomOpenHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given set of migrations on the provided database.
|
||||||
|
* <p>
|
||||||
|
* It uses the same algorithm that Room uses to choose migrations so the migrations instances
|
||||||
|
* that are provided to this method must be sufficient to bring the database from current
|
||||||
|
* version to the desired version.
|
||||||
|
* <p>
|
||||||
|
* After the migration, the method validates the database schema to ensure that migration
|
||||||
|
* result matches the expected schema. Handling of dropped tables depends on the
|
||||||
|
* {@code validateDroppedTables} argument. If set to true, the verification will fail if it
|
||||||
|
* finds a table that is not registered in the Database. If set to false, extra tables in the
|
||||||
|
* database will be ignored (this is the runtime library behavior).
|
||||||
|
*
|
||||||
|
* @param name The database name. You must first create this database via
|
||||||
|
* {@link #createDatabase(String, int)}.
|
||||||
|
* @param version The final version after applying the migrations.
|
||||||
|
* @param validateDroppedTables If set to true, validation will fail if the database has
|
||||||
|
* unknown
|
||||||
|
* tables.
|
||||||
|
* @param migrations The list of available migrations.
|
||||||
|
* @throws IOException If it cannot find the schema for {@code toVersion}.
|
||||||
|
* @throws IllegalStateException If the schema validation fails.
|
||||||
|
*/
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public SupportSQLiteDatabase runMigrationsAndValidate(@NotNull String name,
|
||||||
|
int version,
|
||||||
|
boolean validateDroppedTables,
|
||||||
|
@NotNull
|
||||||
|
Migration... migrations) throws IOException {
|
||||||
|
File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
|
||||||
|
if (!dbPath.exists()) {
|
||||||
|
throw new IllegalStateException("Cannot find the database file for " + name + ". "
|
||||||
|
+ "Before calling runMigrations, you must first create the database via "
|
||||||
|
+ "createDatabase.");
|
||||||
|
}
|
||||||
|
SchemaBundle schemaBundle = loadSchema(version);
|
||||||
|
RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
|
||||||
|
container.addMigrations(getAutoMigrations(mSpecs));
|
||||||
|
container.addMigrations(migrations);
|
||||||
|
DatabaseConfiguration configuration = new DatabaseConfiguration(
|
||||||
|
mInstrumentation.getTargetContext(),
|
||||||
|
name,
|
||||||
|
mOpenFactory,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
RoomDatabase.JournalMode.TRUNCATE,
|
||||||
|
ArchTaskExecutor.getIOThreadExecutor(),
|
||||||
|
ArchTaskExecutor.getIOThreadExecutor(),
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Collections.<Integer>emptySet(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
|
||||||
|
new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
|
||||||
|
// we pass the same hash twice since an old schema does not necessarily have
|
||||||
|
// a legacy hash and we would not even persist it.
|
||||||
|
schemaBundle.getDatabase().getIdentityHash(),
|
||||||
|
schemaBundle.getDatabase().getIdentityHash());
|
||||||
|
return openDatabase(name, roomOpenHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of {@link Migration} of a database that has been generated using
|
||||||
|
* {@link AutoMigration}.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private List<Migration> getAutoMigrations(List<AutoMigrationSpec> userProvidedSpecs) {
|
||||||
|
if (mDatabaseClass == null) {
|
||||||
|
if (userProvidedSpecs.isEmpty()) {
|
||||||
|
// TODO: Detect that there are auto migrations to test when a deprecated
|
||||||
|
// constructor is used.
|
||||||
|
Log.e(TAG, "If you have any AutoMigrations in your implementation, you must use "
|
||||||
|
+ "a non-deprecated MigrationTestHelper constructor to provide the "
|
||||||
|
+ "Database class in order to test them. If you do not have any "
|
||||||
|
+ "AutoMigrations to test, you may ignore this warning.");
|
||||||
|
return new ArrayList<>();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("You must provide the database class in the "
|
||||||
|
+ "MigrationTestHelper constructor in order to test auto migrations.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomDatabase db = Room.getGeneratedImplementation(mDatabaseClass, "_Impl");
|
||||||
|
Set<Class<? extends AutoMigrationSpec>> requiredAutoMigrationSpecs =
|
||||||
|
db.getRequiredAutoMigrationSpecs();
|
||||||
|
return db.getAutoMigrations(
|
||||||
|
createAutoMigrationSpecMap(requiredAutoMigrationSpecs, userProvidedSpecs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps auto migration spec classes to their provided instance.
|
||||||
|
*/
|
||||||
|
private Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> createAutoMigrationSpecMap(
|
||||||
|
Set<Class<? extends AutoMigrationSpec>> requiredAutoMigrationSpecs,
|
||||||
|
List<AutoMigrationSpec> userProvidedSpecs) {
|
||||||
|
Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> specMap = new HashMap<>();
|
||||||
|
if (requiredAutoMigrationSpecs.isEmpty()) {
|
||||||
|
return specMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProvidedSpecs == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"You must provide all required auto migration specs in the "
|
||||||
|
+ "MigrationTestHelper constructor."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Class<? extends AutoMigrationSpec> spec : requiredAutoMigrationSpecs) {
|
||||||
|
boolean found = false;
|
||||||
|
AutoMigrationSpec match = null;
|
||||||
|
for (AutoMigrationSpec provided : userProvidedSpecs) {
|
||||||
|
if (spec.isAssignableFrom(provided.getClass())) {
|
||||||
|
found = true;
|
||||||
|
match = provided;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"A required auto migration spec (" + spec.getCanonicalName() + ") has not"
|
||||||
|
+ " been provided."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
specMap.put(spec, match);
|
||||||
|
}
|
||||||
|
return specMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) {
|
||||||
|
SupportSQLiteOpenHelper.Configuration config =
|
||||||
|
SupportSQLiteOpenHelper.Configuration
|
||||||
|
.builder(mInstrumentation.getTargetContext())
|
||||||
|
.callback(roomOpenHelper)
|
||||||
|
.name(name)
|
||||||
|
.build();
|
||||||
|
SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
|
||||||
|
mManagedDatabases.add(new WeakReference<>(db));
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void finished(Description description) {
|
||||||
|
super.finished(description);
|
||||||
|
for (WeakReference<SupportSQLiteDatabase> dbRef : mManagedDatabases) {
|
||||||
|
SupportSQLiteDatabase db = dbRef.get();
|
||||||
|
if (db != null && db.isOpen()) {
|
||||||
|
try {
|
||||||
|
db.close();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (WeakReference<RoomDatabase> dbRef : mManagedRoomDatabases) {
|
||||||
|
final RoomDatabase roomDatabase = dbRef.get();
|
||||||
|
if (roomDatabase != null) {
|
||||||
|
roomDatabase.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a database connection to be automatically closed when the test finishes.
|
||||||
|
* <p>
|
||||||
|
* This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
|
||||||
|
* {@link org.junit.Rule Rule} annotation.
|
||||||
|
*
|
||||||
|
* @param db The database connection that should be closed after the test finishes.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void closeWhenFinished(@NotNull SupportSQLiteDatabase db) {
|
||||||
|
if (!mTestStarted) {
|
||||||
|
throw new IllegalStateException("You cannot register a database to be closed before"
|
||||||
|
+ " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
|
||||||
|
+ " test rule? (@Rule)");
|
||||||
|
}
|
||||||
|
mManagedDatabases.add(new WeakReference<>(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a database connection to be automatically closed when the test finishes.
|
||||||
|
* <p>
|
||||||
|
* This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
|
||||||
|
* {@link org.junit.Rule Rule} annotation.
|
||||||
|
*
|
||||||
|
* @param db The RoomDatabase instance which holds the database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void closeWhenFinished(@NotNull RoomDatabase db) {
|
||||||
|
if (!mTestStarted) {
|
||||||
|
throw new IllegalStateException("You cannot register a database to be closed before"
|
||||||
|
+ " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
|
||||||
|
+ " test rule? (@Rule)");
|
||||||
|
}
|
||||||
|
mManagedRoomDatabases.add(new WeakReference<>(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SchemaBundle loadSchema(int version) throws IOException {
|
||||||
|
try {
|
||||||
|
return loadSchema(mInstrumentation.getContext(), version);
|
||||||
|
} catch (FileNotFoundException testAssetsIOExceptions) {
|
||||||
|
Log.w(TAG, "Could not find the schema file in the test assets. Checking the"
|
||||||
|
+ " application assets");
|
||||||
|
try {
|
||||||
|
return loadSchema(mInstrumentation.getTargetContext(), version);
|
||||||
|
} catch (FileNotFoundException appAssetsException) {
|
||||||
|
// throw the test assets exception instead
|
||||||
|
throw new FileNotFoundException("Cannot find the schema file in the assets folder. "
|
||||||
|
+ "Make sure to include the exported json schemas in your test assert "
|
||||||
|
+ "inputs. See "
|
||||||
|
+ "https://developer.android.com/training/data-storage/room/"
|
||||||
|
+ "migrating-db-versions#export-schema for details. Missing file: "
|
||||||
|
+ testAssetsIOExceptions.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
private SchemaBundle loadSchema(Context context, int version) throws IOException {
|
||||||
|
String fileName = mAssetsFolder + "/" + version + ".json";
|
||||||
|
try {
|
||||||
|
InputStream input = getClass().getClassLoader()
|
||||||
|
.getResourceAsStream(fileName);
|
||||||
|
return SchemaBundle.deserialize(input);
|
||||||
|
} catch (NullPointerException nullPointerException) {
|
||||||
|
throw new IOException("File not found: " + fileName, nullPointerException);
|
||||||
|
}
|
||||||
|
// InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
|
||||||
|
// return SchemaBundle.deserialize(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||||
|
static TableInfo toTableInfo(EntityBundle entityBundle) {
|
||||||
|
return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
|
||||||
|
toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||||
|
static FtsTableInfo toFtsTableInfo(FtsEntityBundle ftsEntityBundle) {
|
||||||
|
return new FtsTableInfo(ftsEntityBundle.getTableName(), toColumnNamesSet(ftsEntityBundle),
|
||||||
|
ftsEntityBundle.getCreateSql());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||||
|
static ViewInfo toViewInfo(DatabaseViewBundle viewBundle) {
|
||||||
|
return new ViewInfo(viewBundle.getViewName(), viewBundle.createView());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) {
|
||||||
|
if (indices == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
Set<TableInfo.Index> result = new HashSet<>();
|
||||||
|
for (IndexBundle bundle : indices) {
|
||||||
|
result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(),
|
||||||
|
bundle.getColumnNames(), bundle.getOrders()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<TableInfo.ForeignKey> toForeignKeys(
|
||||||
|
List<ForeignKeyBundle> bundles) {
|
||||||
|
if (bundles == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
Set<TableInfo.ForeignKey> result = new HashSet<>(bundles.size());
|
||||||
|
for (ForeignKeyBundle bundle : bundles) {
|
||||||
|
result.add(new TableInfo.ForeignKey(bundle.getTable(),
|
||||||
|
bundle.getOnDelete(), bundle.getOnUpdate(),
|
||||||
|
bundle.getColumns(), bundle.getReferencedColumns()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> toColumnNamesSet(EntityBundle entity) {
|
||||||
|
Set<String> result = new HashSet<>();
|
||||||
|
for (FieldBundle field : entity.getFields()) {
|
||||||
|
result.add(field.getColumnName());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, TableInfo.Column> toColumnMap(EntityBundle entity) {
|
||||||
|
Map<String, TableInfo.Column> result = new HashMap<>();
|
||||||
|
for (FieldBundle bundle : entity.getFields()) {
|
||||||
|
TableInfo.Column column = toColumn(entity, bundle);
|
||||||
|
result.put(column.name, column);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TableInfo.Column toColumn(EntityBundle entity, FieldBundle field) {
|
||||||
|
return new TableInfo.Column(field.getColumnName(), field.getAffinity(),
|
||||||
|
field.isNonNull(), findPrimaryKeyPosition(entity, field), field.getDefaultValue(),
|
||||||
|
TableInfo.CREATED_FROM_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findPrimaryKeyPosition(EntityBundle entity, FieldBundle field) {
|
||||||
|
List<String> columnNames = entity.getPrimaryKey().getColumnNames();
|
||||||
|
int i = 0;
|
||||||
|
for (String columnName : columnNames) {
|
||||||
|
i++;
|
||||||
|
if (field.getColumnName().equalsIgnoreCase(columnName)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MigratingDelegate extends RoomOpenHelperDelegate {
|
||||||
|
private final boolean mVerifyDroppedTables;
|
||||||
|
|
||||||
|
MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) {
|
||||||
|
super(databaseBundle);
|
||||||
|
mVerifyDroppedTables = verifyDroppedTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createAllTables(SupportSQLiteDatabase database) {
|
||||||
|
throw new UnsupportedOperationException("Was expecting to migrate but received create."
|
||||||
|
+ "Make sure you have created the database first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected RoomOpenHelper.ValidationResult onValidateSchema(
|
||||||
|
@NonNull SupportSQLiteDatabase db) {
|
||||||
|
final Map<String, EntityBundle> tables = mDatabaseBundle.getEntitiesByTableName();
|
||||||
|
for (EntityBundle entity : tables.values()) {
|
||||||
|
if (entity instanceof FtsEntityBundle) {
|
||||||
|
final FtsTableInfo expected = toFtsTableInfo((FtsEntityBundle) entity);
|
||||||
|
final FtsTableInfo found = FtsTableInfo.read(db, entity.getTableName());
|
||||||
|
if (!expected.equals(found)) {
|
||||||
|
return new ValidationResult(false, expected.name
|
||||||
|
+ "\nExpected: " + expected + "\nFound: " + found);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final TableInfo expected = toTableInfo(entity);
|
||||||
|
final TableInfo found = TableInfo.read(db, entity.getTableName());
|
||||||
|
if (!expected.equals(found)) {
|
||||||
|
return new ValidationResult(false, expected.name
|
||||||
|
+ "\nExpected: " + expected + " \nfound: " + found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (DatabaseViewBundle view : mDatabaseBundle.getViews()) {
|
||||||
|
final ViewInfo expected = toViewInfo(view);
|
||||||
|
final ViewInfo found = ViewInfo.read(db, view.getViewName());
|
||||||
|
if (!expected.equals(found)) {
|
||||||
|
return new ValidationResult(false, expected
|
||||||
|
+ "\nExpected: " + expected + " \nfound: " + found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mVerifyDroppedTables) {
|
||||||
|
// now ensure tables that should be removed are removed.
|
||||||
|
Set<String> expectedTables = new HashSet<>();
|
||||||
|
for (EntityBundle entity : tables.values()) {
|
||||||
|
expectedTables.add(entity.getTableName());
|
||||||
|
if (entity instanceof FtsEntityBundle) {
|
||||||
|
expectedTables.addAll(((FtsEntityBundle) entity).getShadowTableNames());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type='table'"
|
||||||
|
+ " AND name NOT IN(?, ?, ?)",
|
||||||
|
new String[]{Room.MASTER_TABLE_NAME, "android_metadata",
|
||||||
|
"sqlite_sequence"});
|
||||||
|
//noinspection TryFinallyCanBeTryWithResources
|
||||||
|
try {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
final String tableName = cursor.getString(0);
|
||||||
|
if (!expectedTables.contains(tableName)) {
|
||||||
|
return new ValidationResult(false, "Unexpected table "
|
||||||
|
+ tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ValidationResult(true, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CreatingDelegate extends RoomOpenHelperDelegate {
|
||||||
|
|
||||||
|
CreatingDelegate(DatabaseBundle databaseBundle) {
|
||||||
|
super(databaseBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createAllTables(SupportSQLiteDatabase database) {
|
||||||
|
for (String query : mDatabaseBundle.buildCreateQueries()) {
|
||||||
|
database.execSQL(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected RoomOpenHelper.ValidationResult onValidateSchema(
|
||||||
|
@NonNull SupportSQLiteDatabase db) {
|
||||||
|
throw new UnsupportedOperationException("This open helper just creates the database but"
|
||||||
|
+ " it received a migration request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate {
|
||||||
|
final DatabaseBundle mDatabaseBundle;
|
||||||
|
|
||||||
|
RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
|
||||||
|
super(databaseBundle.getVersion());
|
||||||
|
mDatabaseBundle = databaseBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dropAllTables(SupportSQLiteDatabase database) {
|
||||||
|
throw new UnsupportedOperationException("cannot drop all tables in the test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(SupportSQLiteDatabase database) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onOpen(SupportSQLiteDatabase database) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
|
||||||
|
object RobolectricMigrationTestHelperFactory : SharedMigrationTestRuleFactory {
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
RobolectricMigrationTestHelper(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
RobolectricMigrationTestHelper(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass,
|
||||||
|
specs
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>,
|
||||||
|
openFactory: SupportSQLiteOpenHelper.Factory
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
RobolectricMigrationTestHelper(
|
||||||
|
instrumentation,
|
||||||
|
databaseClass,
|
||||||
|
specs,
|
||||||
|
openFactory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,7 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
|
||||||
|
|
||||||
override fun createSnackbarVerification(): SnackbarVerificationTestRule =
|
override fun createSnackbarVerification(): SnackbarVerificationTestRule =
|
||||||
RobolectricSnackbarVerificationTestRule
|
RobolectricSnackbarVerificationTestRule
|
||||||
|
|
||||||
|
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
|
||||||
|
RobolectricMigrationTestHelperFactory
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package org.fnives.test.showcase.storage.migration
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.fnives.test.showcase.storage.LocalDatabase
|
||||||
|
import org.fnives.test.showcase.storage.favourite.FavouriteEntity
|
||||||
|
import org.fnives.test.showcase.storage.migation.Migration1To2
|
||||||
|
import org.fnives.test.showcase.testutils.configuration.SharedMigrationTestRule
|
||||||
|
import org.fnives.test.showcase.testutils.configuration.createSharedMigrationTestRule
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reference:
|
||||||
|
* https://medium.com/androiddevelopers/testing-room-migrations-be93cdb0d975
|
||||||
|
* https://developer.android.com/training/data-storage/room/migrating-db-versions
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MigrationToLatest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val helper: SharedMigrationTestRule = createSharedMigrationTestRule<LocalDatabase>(
|
||||||
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
|
emptyList(),
|
||||||
|
FrameworkSQLiteOpenHelperFactory()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getMigratedRoomDatabase(): LocalDatabase {
|
||||||
|
val database: LocalDatabase = Room.databaseBuilder(
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
LocalDatabase::class.java,
|
||||||
|
TEST_DB
|
||||||
|
)
|
||||||
|
.addMigrations(Migration1To2())
|
||||||
|
.build()
|
||||||
|
// close the database and release any stream resources when the test finishes
|
||||||
|
helper.closeWhenFinished(database)
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
stopKoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun migrate1To2() {
|
||||||
|
val expectedEntities = setOf(
|
||||||
|
FavouriteEntity("123"),
|
||||||
|
FavouriteEntity("124"),
|
||||||
|
FavouriteEntity("125")
|
||||||
|
)
|
||||||
|
val version1DB = helper.createDatabase(
|
||||||
|
name = TEST_DB,
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
version1DB.run {
|
||||||
|
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (\"123\")")
|
||||||
|
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (124)")
|
||||||
|
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (125)")
|
||||||
|
}
|
||||||
|
version1DB.close()
|
||||||
|
|
||||||
|
val version2DB = helper.runMigrationsAndValidate(
|
||||||
|
name = TEST_DB,
|
||||||
|
version = 2,
|
||||||
|
validateDroppedTables = true,
|
||||||
|
Migration1To2()
|
||||||
|
)
|
||||||
|
version2DB.close()
|
||||||
|
|
||||||
|
val favouriteDao = getMigratedRoomDatabase().favouriteDao
|
||||||
|
|
||||||
|
val entities = runBlocking { favouriteDao.get().first() }.toSet()
|
||||||
|
|
||||||
|
Assert.assertEquals(expectedEntities, entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_DB = "migration-test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
interface SharedMigrationTestRule : TestRule {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun createDatabase(name: String, version: Int): SupportSQLiteDatabase
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun runMigrationsAndValidate(
|
||||||
|
name: String,
|
||||||
|
version: Int,
|
||||||
|
validateDroppedTables: Boolean,
|
||||||
|
vararg migrations: Migration
|
||||||
|
): SupportSQLiteDatabase
|
||||||
|
|
||||||
|
fun closeWhenFinished(db: SupportSQLiteDatabase)
|
||||||
|
fun closeWhenFinished(db: RoomDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified DB : RoomDatabase> createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
|
||||||
|
.createSharedMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
DB::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun <reified DB : RoomDatabase> createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
specs: List<AutoMigrationSpec>
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
|
||||||
|
.createSharedMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
DB::class.java,
|
||||||
|
specs
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun <reified DB : RoomDatabase> createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
specs: List<AutoMigrationSpec>,
|
||||||
|
openFactory: SupportSQLiteOpenHelper.Factory
|
||||||
|
): SharedMigrationTestRule =
|
||||||
|
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
|
||||||
|
.createSharedMigrationTestRule(
|
||||||
|
instrumentation,
|
||||||
|
DB::class.java,
|
||||||
|
specs,
|
||||||
|
openFactory
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.fnives.test.showcase.testutils.configuration
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
|
||||||
|
interface SharedMigrationTestRuleFactory {
|
||||||
|
|
||||||
|
fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
): SharedMigrationTestRule
|
||||||
|
|
||||||
|
fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>
|
||||||
|
): SharedMigrationTestRule
|
||||||
|
|
||||||
|
fun createSharedMigrationTestRule(
|
||||||
|
instrumentation: Instrumentation,
|
||||||
|
databaseClass: Class<out RoomDatabase>,
|
||||||
|
specs: List<AutoMigrationSpec>,
|
||||||
|
openFactory: SupportSQLiteOpenHelper.Factory
|
||||||
|
): SharedMigrationTestRule
|
||||||
|
}
|
||||||
|
|
@ -15,4 +15,6 @@ interface TestConfigurationsFactory {
|
||||||
fun createLoginRobotConfiguration(): LoginRobotConfiguration
|
fun createLoginRobotConfiguration(): LoginRobotConfiguration
|
||||||
|
|
||||||
fun createSnackbarVerification(): SnackbarVerificationTestRule
|
fun createSnackbarVerification(): SnackbarVerificationTestRule
|
||||||
|
|
||||||
|
fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue