Skip to main content

Testing Migrations

Learn how to write comprehensive migration tests with custom validation to ensure your SwiftData migrations preserve data integrity.

Overview

FreezeRay helps you write comprehensive migration tests with custom validation to ensure your SwiftData migrations preserve data integrity. What you’ll learn:
  • How to generate migration tests early in your workflow (new!)
  • How scaffolded migration tests work
  • Adding custom data validation
  • Testing edge cases (empty databases, large datasets)
  • Common migration pitfalls

Two Workflows: Old vs. New

❌ Old Workflow (generate after)

Problem: Migration test generated after you’ve already written one.

✅ New Workflow (generate early)

Benefit: Migration test created when you need it for development.
Use freezeray generate migration-tests to create the test when you start working on the migration, not after!

Prerequisites

  • You’ve frozen at least one schema version (e.g., 1.0.0)
  • You have a SchemaMigrationPlan defined (or plan to create one)
  • You’re ready to start working on the next schema version

The Scaffolded Migration Test

You can generate a migration test in two ways:
  1. Early (recommended): freezeray generate migration-tests -f 1.0.0 -t 2.0.0
  2. Automatically: freezeray freeze 2.0.0 (if 1.0.0 fixtures exist)
Either way, you get:
Generated tests now use @Suite(.serialized) to prevent parallel execution issues with SwiftData stores. This means tests work in the default test harness without special configuration!

What This Test Does

  1. Loads the frozen v1.0.0 SQLite database from FreezeRay/Fixtures/1.0.0/App-1_0_0.sqlite
  2. Copies it to a temporary location (bundle resources are read-only)
  3. Creates a ModelContainer with v2.0.0 schema and your migration plan
  4. Runs migration from v1.0.0 → v2.0.0
  5. Throws if migration crashes
If migration succeeds, the test passes. But it doesn’t verify data integrity - that’s your job!
The scaffolded test only checks that migration doesn’t crash. It doesn’t verify data is preserved correctly.

Adding Custom Validation

Let’s add data validation to ensure migration preserves user data.

Example Migration Scenario

Schema V1 (v1.0.0):
Schema V2 (v2.0.0):
Migration:

Step 1: Seed Test Data in V1

Before freezing v1.0.0, add test data to your frozen database:
You can seed data either manually (via Xcode’s data model editor) or programmatically before freezing.

Step 2: Add Validation to Migration Test

Update the scaffolded test with custom checks:

What This Validation Checks

  1. Record count - No users were lost during migration
  2. Existing fields - name and email are preserved
  3. New fields - createdAt has default values (not nil)
  4. Data types - Fields have sensible values

Common Validation Patterns

Pattern 1: Verify Record Counts

Ensure no data is lost:

Pattern 2: Check Required Fields

Ensure non-optional fields have values:

Pattern 3: Validate New Fields

Check that new fields have default values or proper initialization:

Pattern 4: Verify Relationships

Ensure relationships are intact after migration:

Pattern 5: Check Data Transformations

If migration transforms data, verify correctness:

Testing Custom Migrations

For custom migrations with didMigrate logic, test your transformation code:

Custom Migration Example

Test Custom Logic

Testing Edge Cases

Empty Database

Test that migration works with no data:

Large Dataset

Test performance with many records:

Multi-Hop Migrations

Test migrations that span multiple versions: V1 → V2 → V3
Multi-hop migrations test cumulative effects of multiple migrations. Users might skip app versions, so always test the full path.

Common Migration Issues

Issue 1: Data Loss in Custom Migrations

Symptom: Records disappear after migration Cause:
Fix:
Test:

Issue 2: Nil Values for Non-Optional Fields

Symptom: Optional fields become nil unexpectedly Cause: SwiftData can’t provide defaults for new non-optional fields Fix:
Test:

Issue 3: Broken Relationships

Symptom: Relationships are nil after migration Cause: Inverse relationships not maintained Fix: Use SwiftData’s @Relationship properly with inverses Test:

Best Practices

Always add custom validation. The scaffolded test only checks for crashes, not data correctness.

1. Test with Real Data

Use your frozen fixtures with realistic data:

2. Document Expected Behavior

Add comments explaining what each validation checks:

3. Test Edge Cases

Don’t just test happy paths:

4. Run Tests in CI

Migration tests should run on every PR:

Troubleshooting

”Fixture not found” error

Cause: Fixtures not included in test bundle Fix:
  1. In Xcode, select your test target
  2. Build Phases → Copy Bundle Resources
  3. Add FreezeRay/ folder (ensure it’s a folder reference, not group)

Migration test passes locally, fails in CI

Cause: Fixtures not committed to git Fix:

Test crashes with “cannot open database”

Cause: Trying to write to read-only bundle resource Fix: Always copy fixture to writable location:

Summary

You’ve learned how to:
  • ✅ Understand scaffolded migration tests
  • ✅ Add custom data validation
  • ✅ Test edge cases (empty DB, large datasets)
  • ✅ Verify multi-hop migrations
  • ✅ Catch common migration issues
Migration testing catches data corruption before it reaches production!

Next Steps

CI Integration

Run migration tests in your CI pipeline

Migration Testing Concepts

Deep dive into how migration testing works

First Freeze

Tutorial for freezing your first schema

Drift Detection

Understand drift detection mechanics