Skip to main content

App Update Testing: Migration and Data Integrity

Validate Android and iOS app upgrades with old local data, pending work, interrupted installs and backend compatibility.

R
Written by Reiko

Fresh-install tests never exercise the saved filters carried over from an older app build. A schema change can also leave queued uploads in the old record format. The difficult upgrade path begins with data written by an earlier version, possibly months earlier, and ends only when the new app has completed its first real task without losing or misassigning that data.

Build starting states from actual old releases

Install a supported previous release, create data through its UI and update the same installation to the candidate build. A clean install of the candidate cannot test a migration. Include the oldest version from which direct upgrade is supported, a recent version and a version with a known historical schema change. Keep the old builds and matching test data available to the team.

Prepare accounts with a saved draft, a completed record, cached media and pending offline work. Use distinctive markers such as “draft A17” so a tester can distinguish the original from a recreated default. If the product supports multiple signed-in accounts or profiles, set up two and verify ownership after the update.

Starting state

Upgrade check

Failure signal

Old local database

Open detail, edit and restart

Record missing or field mapped incorrectly

Queued upload

Reconnect after update

Duplicate item or wrong account ownership

Stored session

Open authenticated route

Unexpected loop or unsafe access

Large cache

Launch with little free storage

Partial migration damages valid data

Old deep link

Open link after update

Blank destination or wrong content

The table defines test cases, not a guarantee that every user keeps the same session or cache. An app may intentionally require reauthentication or rebuild disposable cache. The important part is to specify that behavior and verify durable user data separately.

Make migration success observable

A schema version number only says which migration code should run. It does not prove that records survived. Count old records, compare selected field values, check relationships and open the updated content through the UI. If a migration converts a timestamp or currency field, compare known edge values rather than merely verifying that the app launches.

Use a fixture containing an empty string, a long Unicode value, an optional field, a deleted parent and a record created just before the old release's shutdown. These inputs exercise assumptions about nulls, encoding and referential integrity. When a data model changes from one record per item to multiple child rows, check that no child is silently duplicated.

Run the same migration twice in a controlled test environment. A second startup should not repeat non-idempotent conversions. If the first run stops halfway because storage is full or the process exits, the next launch should resume safely or roll back to a readable prior state according to the storage design.

Interrupt the first launch, not just installation

Many update failures happen after the store installs the package, when the new app first reads and transforms data. Start the updated app with low free storage, then interrupt it while migration is underway using a test build that exposes a deterministic pause point. Relaunch and inspect both raw storage and visible records. Do not rely on random process killing to hit the same boundary each time.

Test failure injection before a migration step, after it writes a subset of records and after it records completion. A transaction may make some steps atomic, but large file moves or remote updates can fall outside that transaction. Verify the recovery design rather than assuming a database transaction protects every file and server operation.

For a release candidate, measure first-launch duration on an older supported phone with a realistically populated database. A migration that succeeds after a minute of unresponsive UI may still be unacceptable. Keep long work off the interactive path where possible, show accurate progress if work must block, and test cancellation or backgrounding.

Coordinate old and new clients with the backend

Users do not all update at once. While the new build is live, older supported builds may continue sending requests. Test an old client against the new server, and the new client against the server state that exists before a coordinated rollout. If the API changes, choose a compatibility window or versioning rule and verify it with actual requests.

Put a queued command from the old app on the device, update before reconnecting and then send it. The command may have an old payload shape, token or idempotency identifier. Confirm it is upgraded or translated without changing its meaning. Replaying it as a fresh command can duplicate work; dropping it without explanation can lose a user's action.

Account identity deserves a separate check. Sign out on the old version with pending work, upgrade and sign in as a different test account. No data or queue item from the first account should appear in the second. If the product keeps offline drafts across sign-out, test how it labels and restricts those drafts explicitly.

Do not treat rollback as an ordinary user path

App stores and operating systems do not provide a universal safe downgrade for every installed app. A new data schema may be unreadable to an older binary. Plan a corrective forward release and server-side compatibility controls for critical failures. In a laboratory you can test restoring an old backup or old binary, but a successful laboratory downgrade is not proof that users can do the same.

During a staged rollout, collect failures by app version, old-version origin and migration step. A general crash-free rate can hide a defect that affects only users upgrading from one old schema. Stop or adjust the rollout if the affected path breaches the release gate; then verify the fix with the exact old build and data fixture.

Keep an intact backup or a non-destructive migration path for data whose loss would be costly. A server backup cannot recover a draft that existed only on one phone. Document which records are local-only and give those paths their own release checks.

Retest the first useful task after conversion

After the update, open an old record, change it, save it, restart and confirm the change survives. Then sync to another device where applicable. This checks both reading the old format and writing the new format. Repeat with an interrupted write and a full device restart. A migration can display old records correctly while creating malformed new revisions.

Verify search indexes, thumbnails and notifications if they refer to migrated IDs. Derived caches may be rebuilt, but references must not point to the wrong record. Track record identity through the old database, new database, backend response and UI, rather than judging by visual similarity alone.

Keep an upgrade evidence bundle

For each candidate build, record the old app version, target version, starting fixture, schema versions, device and OS, test account, migration duration and resulting record counts. Take a snapshot of non-sensitive fixture identifiers before and after. This lets a later failure be compared with a known passing route rather than reconstructed from memory.

Include a test of a device backup restored onto a supported new phone if the product claims that route. Fresh-install login followed by server sync is not the same as restoring local app data. Check whether local-only drafts, encryption keys and secure storage remain usable under the supported restore path. If an item intentionally does not transfer, the UI should describe the limitation without showing stale references.

When a migration defect is fixed, replay the exact starting fixture that exposed it and a neighbouring fixture from another old release. Check the fix on a second cold launch as well as the first. Keep the original affected dataset out of logs or use synthetic data with the same schema shape.

Rehearse the rollout with old installs

Prepare an install from the oldest supported direct-upgrade version and another from the latest production build. Put a pending upload and a local-only draft on both, then install the release candidate without wiping app data. Run the first launch, interrupt a migration at a controlled boundary and compare the old and new record counts. The two origins can expose different migration paths.

After the app becomes usable, edit an old record, sync it and restart. Check server compatibility using an older still-supported client at the same time. Record the exact build combination so a staged rollout failure can be reproduced with an old data fixture rather than a clean install.

Upgrade testing questions

Why is a fresh install insufficient?

It starts with no old records, old queue entries or previous session state. The migration code may never run.

Should every older release be tested?

Cover the supported direct-upgrade boundary, major schema transitions and recent releases. Expand the set when telemetry or a past defect shows another risky origin.

What counts as a completed migration test?

Preserved records are opened, edited, saved, relaunched and reconciled with the backend where appropriate. A successful launch alone is only the first step.

Did this answer your question?