Migration Testing mit Room
Lerne, wie du Room-Migrationen prüfst, damit App-Updates Daten erhalten und Schemaänderungen kontrolliert bleiben.
Migration Testing bedeutet, dass du Datenbankänderungen nicht nur implementierst, sondern als Update-Szenario prüfst. Gerade bei Room ist das wichtig: Deine App wird nicht immer frisch installiert, sondern oft von Version zu Version aktualisiert. Dabei müssen Tabellen, Spalten, Indizes und vorhandene Nutzerdaten zusammenpassen.
Was ist das?
Migration Testing ist das Testen eines Datenbank-Upgrades von einem alten Schema auf ein neues Schema. Im Android-Kontext betrifft das häufig Room, also die Jetpack-Bibliothek für SQLite-Zugriff mit Kotlin. Sobald du eine Entity änderst, eine neue Tabelle ergänzt oder eine Spalte umbenennst, verändert sich das Schema deiner lokalen Datenbank. Für Nutzerinnen und Nutzer darf das Update aber nicht bedeuten, dass gespeicherte Aufgaben, Nachrichten, Einstellungen oder Offline-Daten verschwinden.
Das mentale Modell ist einfach: Eine Datenbank hat eine Versionsnummer. Deine App-Version enthält Code, der mit einer bestimmten Datenbankversion arbeiten will. Wenn auf dem Gerät noch eine ältere Datenbank liegt, muss Room wissen, wie es von Version A nach Version B kommt. Migration Testing prüft genau diese Brücke.
In einer modernen Android-App sitzt die Datenbank meist in der Data Layer. Repositorys greifen auf DAOs zu, ViewModels liefern State an Compose, und die UI zeigt Daten an, ohne zu wissen, ob sie aus Netzwerk, Cache oder SQLite stammen. Gerade bei Offline-First-Apps ist die lokale Datenbank oft die verlässliche Quelle für den aktuellen App-Zustand. Wenn eine Migration fehlschlägt, ist nicht nur ein technisches Detail kaputt: Der Nutzer kann Daten verlieren oder die App startet nach dem Update nicht mehr.
Wie funktioniert es?
Room arbeitet mit einer Datenbankversion, die du in deiner RoomDatabase angibst. Änderst du das Schema, erhöhst du diese Version. Dann definierst du eine Migration, die SQL-Anweisungen ausführt, um die alte Struktur in die neue Struktur zu überführen. Typische Schritte sind ALTER TABLE, CREATE TABLE, Daten kopieren, alte Tabellen entfernen oder Indizes neu anlegen.
Ein Migrationstest bildet den realen Upgrade-Pfad nach. Er startet mit einer Datenbank im alten Schema, schreibt Testdaten hinein, führt die Migration aus und prüft danach zwei Dinge: Das neue Schema ist gültig, und die Daten sind noch korrekt vorhanden. Dafür bietet Room Hilfen wie MigrationTestHelper, wenn du Schemas exportierst. Diese exportierten Schema-Dateien sind wichtig, weil sie eine prüfbare Beschreibung deiner Datenbankversionen liefern.
Im Alltag erscheint Migration Testing immer dann, wenn du ein Datenmodell änderst. Du ergänzt etwa bei einer NoteEntity ein Feld archived, führst eine neue Tabelle für Synchronisationsstatus ein oder normalisierst eine alte Tabelle. Ohne Test erkennst du oft erst spät, ob bestehende Installationen korrekt aktualisiert werden. Ein frischer Emulator-Installationslauf reicht nicht, weil er nur das aktuelle Schema erzeugt. Er simuliert nicht den Zustand einer echten Nutzerinstallation.
Wichtig ist auch der Upgrade-Pfad über mehrere Versionen. Wenn deine App von Datenbankversion 1 auf 4 aktualisiert werden kann, müssen entweder alle Schritte 1 -> 2, 2 -> 3, 3 -> 4 vorhanden sein oder du brauchst eine gültige direkte Migration. In echten Releases überspringen manche Nutzer mehrere App-Versionen. Dein Test sollte deshalb nicht nur den neuesten Schritt prüfen, sondern die Pfade, die du in Produktion unterstützen willst.
In der Praxis
Ein typischer Room-Migrationstest sieht stark vereinfacht so aus:
@RunWith(AndroidJUnit4::class)
class AppDatabaseMigrationTest {
@get:Rule
val helper = MigrationTestHelper(
instrumentation = InstrumentationRegistry.getInstrumentation(),
databaseClass = AppDatabase::class.java,
specs = emptyList()
)
@Test
fun migrate1To2_keepsExistingNotes() {
val dbName = "migration-test"
helper.createDatabase(dbName, 1).apply {
execSQL(
"INSERT INTO notes (id, title, body) VALUES (1, 'Room', 'Migration testen')"
)
close()
}
val db = helper.runMigrationsAndValidate(
dbName,
2,
true,
MIGRATION_1_2
)
val cursor = db.query("SELECT id, title, body, archived FROM notes WHERE id = 1")
assertThat(cursor.moveToFirst()).isTrue()
assertThat(cursor.getString(cursor.getColumnIndexOrThrow("title"))).isEqualTo("Room")
assertThat(cursor.getInt(cursor.getColumnIndexOrThrow("archived"))).isEqualTo(0)
cursor.close()
}
}
Die passende Migration könnte so aussehen:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE notes ADD COLUMN archived INTEGER NOT NULL DEFAULT 0")
}
}
Die praktische Regel lautet: Teste nicht nur, ob die Migration ohne Crash durchläuft. Teste fachliche Daten. Lege im alten Schema genau die Daten an, die dir wichtig sind, und prüfe nach dem Upgrade die Werte, die deine App später wirklich braucht. Bei einer Offline-First-App können das lokale Änderungen, Synchronisationsmarker oder Zeitstempel sein.
Eine typische Stolperfalle ist fallbackToDestructiveMigration(). Diese Option kann während der frühen Entwicklung bequem wirken, löscht aber bei fehlender Migration die bestehende Datenbank. Für produktive Apps ist das nur in sehr bewusst gewählten Fällen vertretbar. Wenn Nutzerdaten lokal wichtig sind, sollte eine fehlende Migration im Test auffallen, nicht durch Löschen verdeckt werden.
Eine zweite Stolperfalle sind Default-Werte. Wenn du eine NOT NULL-Spalte ergänzt, brauchen bestehende Zeilen einen sinnvollen Standardwert. Room und SQLite können keine nicht-nullbare Spalte ohne Wert für alte Datensätze herbeizaubern. Genau solche Fälle gehören in deinen Migrationstest.
Fazit
Migration Testing macht Datenbankänderungen releasefähig. Du prüfst damit, ob ein Room-Schema-Upgrade nicht nur technisch gültig ist, sondern vorhandene Nutzerdaten schützt. Nimm dir bei der nächsten Entity-Änderung eine konkrete alte Datenbankversion, schreibe repräsentative Daten hinein, führe die Migration aus und kontrolliere das Ergebnis im Test oder Debugger. Im Code-Review solltest du bei jeder Schemaänderung fragen: Welche Daten existieren bereits auf Geräten, und welcher Test beweist, dass sie nach dem Update noch nutzbar sind?