room-migration-data-survival-test
Prove a Room upgrade keeps the user's DATA, not just that the schema validates — covers the passing migration test that only seeds one settings/profile row, which any migration that drops user tables
Files1
View on GitHubSKILL.mdskills/managed/room-migration-data-survival-test/SKILL.md
GitHubRoom migration data-survival test
runMigrationsAndValidate proves the schema matches. It says nothing about
whether rows survived. A migration test that seeds one settings row and checks
it back is the classic false comfort: a migration that drops sessions,
set_logs, or any other user table passes it unchanged.
Losing logged user data on an app update is usually the worst bug a local-first app can ship, and it is invisible until a real install upgrades.
Procedure
Read what the existing test asserts, not its name. If it seeds only a profile/settings row, the data axis is uncovered however thorough it looks.
Find the exported baseline schema (
app/schemas/<db>/<N>.json) and read the real column names/nullability of the user tables from it — not from today's entity classes, which have moved on.s = json.loads(pathlib.Path("app/schemas/<db>/21.json").read_text()) for e in s["database"]["entities"]: print(e["tableName"], [f["columnName"] for f in e["fields"]])Seed real training/user rows at the shipped baseline with
helper.createDatabase(name, BASELINE).use { it.execSQL(...) }. Cover:- a parent row and two child rows (so a count assertion can distinguish "some" from "all"),
- at least one field that is device-only/private — its silent loss is the one a user notices last,
- one row per additional user table (unlocks, measurements, …).
Walk the whole registered chain to
VERSION, then read each row back and assert values, not justmoveToFirst(). Counts plus aSUM/MAXcatch partial loss that a row-exists check misses.Mutation-prove it. Insert one destructive line into a real migration and confirm the named assertion fails:
db.execSQL("DELETE FROM set_logs") // inside MIGRATION_N_N+1Expect
both logged sets must survive expected:<2> but was:<0>. Restore the file and confirm byte-identical (read == orig) before moving on.
Traps
- Stale results XML. Delete
app/build/outputs/androidTest-resultsbefore each run; a failed compile otherwise re-reports the previous green. - Asserting only the current version. A test that creates the DB at
VERSIONand migrates toVERSIONis a no-op walk; start at the oldest schema you actually shipped. - Row-exists assertions.
assertTrue(c.moveToFirst())passes when half the rows are gone. Assert counts and aggregates. - Schema drift in the seed SQL. If
execSQLfails at the baseline, the column list came from the entity class, not the exported JSON.