Как построить проверку миграций базы iOS на облачном Mac

Как построить проверку миграций базы iOS на облачном Mac

На первый взгляд безобидное изменение поля Core Data обычно не вызывает проблем в симуляторе с чистой установкой, но может оставить пользователей, обновляющихся напрямую со старой версии, на экране запуска. В CI нужно проверять не создание пустой базы, а возможность миграции старого хранилища по поддерживаемому пути, сохранность ключевых данных и наличие достаточной диагностической информации при сбое. Постоянно доступный облачный Mac хорошо подходит для выделенной проверки таких длительных, но детерминированных сценариев.

Сначала определите границы поддержки миграций

Сначала перечислите все версии базы данных, которые еще могут встречаться у пользователей. Не исходите из того, что приложение всегда обновляют последовательно от версии к версии. Если текущая модель — V5, а в эксплуатации все еще остаются клиенты V3 и V4, необходимо как минимум проверить переходы V3→V5 и V4→V5. Тест пустой базы V5 подтверждает лишь возможность загрузить текущую модель, но не выявляет проблемы с переименованием старых полей, изменением ограничений связей или конфликтами уникальных индексов.

Рекомендуется вести простую матрицу:

Образец Целевая версия Обязательные проверки
Базовая база V3 V5 Количество учетных записей, проектов и исторических задач
Пограничная база V4 V5 Пустые связи, повторяющиеся имена, отметки об удалении
Большой образец V4 V5 Время завершения миграции и пиковый размер файлов
Пустая база V5 V5 Инициализация текущей модели

Критерием прохождения проверки миграции должно быть сохранение семантики данных, а не просто отсутствие ошибки при загрузке постоянного хранилища.

Для каждого образца также следует зафиксировать версию приложения, в которой он был создан, идентификатор модели, ожидаемое количество записей и контрольную сводку. Не создавайте образцы из базы, которой разработчик пользуется в повседневной работе: ее содержимое меняется вместе с действиями пользователя, поэтому воспроизвести сбой будет сложно.

Зафиксируйте воспроизводимые образцы старых баз

Для каждой исторической версии подготовьте специальную сборку, записывающую заранее определенный набор данных. В него должны входить обычные записи, пустые значения, длинные тексты, архивированные объекты и объекты со значениями, близкими к границам ограничений. После записи корректно закройте постоянное хранилище и только затем скопируйте базу данных.

При использовании SQLite в режиме WAL данные могут быть распределены между тремя файлами. Надежнее всего дождаться сохранения данных приложением и выполнить checkpoint. Если гарантировать это невозможно, архивируйте всю группу файлов:

set -euo pipefail

STORE="$HOME/Library/Developer/CoreSimulator/Devices/$SIM_UDID/data/Containers/Data/Application/$APP_ID/Library/Application Support/App.sqlite"
OUT="MigrationFixtures/V4"

mkdir -p "$OUT"
cp "$STORE" "$OUT/App.sqlite"

for suffix in -wal -shm; do
  if [ -f "${STORE}${suffix}" ]; then
    cp "${STORE}${suffix}" "$OUT/App.sqlite${suffix}"
  fi
done

find "$OUT" -type f -print0 | sort -z | xargs -0 shasum -a 256 > "$OUT/SHA256SUMS"

Не открывайте исходный образец из репозитория непосредственно в тесте. Перед каждым тестом копируйте его во временный каталог, а после миграции удаляйте копию, чтобы параллельные задания не изменяли данные друг друга. Если образец содержит реальные пользовательские данные, токены или сведения о подключениях, создайте новый анонимизированный набор вместо того, чтобы полагаться на последующее обезличивание.

Оформите миграцию как тестируемую точку входа

При запуске приложения загрузка базы данных часто связана с инициализацией интерфейса и сетевыми запросами, что затрудняет локализацию ошибок миграции. Создание постоянного хранилища следует вынести в компонент с внедряемым URL, чтобы XCTest мог напрямую загружать временную копию.

func makeContainer(storeURL: URL) throws -> NSPersistentContainer {
    let container = NSPersistentContainer(name: "AppModel")
    let description = NSPersistentStoreDescription(url: storeURL)
    description.shouldMigrateStoreAutomatically = true
    description.shouldInferMappingModelAutomatically = true
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    container.persistentStoreDescriptions = [description]

    var loadError: Error?
    container.loadPersistentStores { _, error in
        loadError = error
    }
    if let loadError {
        throw loadError
    }
    return container
}

Облегченная миграция подходит для добавления необязательных полей, атрибутов со значениями по умолчанию и явно заданных переименований. При разделении сущностей, объединении данных или преобразовании значений необходимо использовать явное сопоставление либо поэтапную миграцию. Не удаляйте старые модели только ради прохождения тестов: исторические модели необходимы для чтения метаданных старых баз и определения пути миграции.

Проверяйте бизнес-инварианты

После миграции проверьте как минимум четыре уровня: успешную загрузку постоянного хранилища, обновление версии модели, соответствие количества записей ожиданиям, а также корректность ключевых связей и семантики полей. Например, общее число задач не должно меняться, архивированные проекты должны оставаться архивированными, а количество осиротевших дочерних объектов должно быть равно нулю.

При сравнении не полагайтесь исключительно на автоматически создаваемые идентификаторы объектов. Записывайте в тестовые данные стабильные бизнес-ключи и проверяйте поля по ним. Перед сравнением упорядоченных данных или связей-множеств нормализуйте их, чтобы несущественные различия в порядке не приводили к случайным сбоям.

Запускайте отдельное тестовое задание на облачном Mac

Тесты миграции следует выполнять отдельно от обычных модульных тестов, поскольку они копируют образцы и многократно создают постоянные хранилища. Зафиксируйте среду выполнения симулятора и название целевого устройства, сначала запустите устройство, а затем выполните назначенный план тестирования:

set -euo pipefail

xcrun simctl bootstatus "$SIM_UDID" -b

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App \
  -testPlan DatabaseMigration \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath Artifacts/MigrationTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

Перед началом задания очищайте временный каталог. После завершения независимо от результата сохраняйте xcresult, журналы миграции, сводку образца и идентификатор целевой модели. Не публикуйте мигрированную базу целиком как стандартный артефакт: она может содержать тестовые секреты и быстро увеличит объем хранилища. Обычно анонимизированную копию образца сохраняют только при сбое и удаляют по явно заданному расписанию.

При запуске на ZoomMini привяжите задание к конкретной версии инструментов Xcode и проверьте доступные конфигурации в панели управления. После смены Xcode или среды выполнения симулятора сначала отдельно запустите план миграции и лишь затем возвращайте его в полный конвейер. Так изменения инструментов и модели не попадут в сборку одновременно и не усложнят поиск причины сбоя.

Типичные сбои и контрольный список перед выпуском

Ситуация, когда тесты проходят локально, но завершаются сбоем в CI, обычно связана с неполным копированием группы файлов, изменением образца предыдущим тестом, отсутствием ресурсов модели в тестовом пакете или одновременной работой нескольких тестов с одним путем. Сначала сверьте SHA-256, затем убедитесь, что временный каталог уникален, и наконец проверьте наличие всех поддерживаемых версий модели в результатах сборки.

Перед выпуском последовательно проверьте следующее:

  • для каждой поддерживаемой исходной версии есть фиксированный образец и ожидаемая сводка;
  • основной файл SQLite согласован с состоянием WAL;
  • тесты мигрируют только временные копии и не изменяют исходные fixture-файлы;
  • границы применимости облегченной миграции и явного сопоставления документированы;
  • для количества записей, стабильных бизнес-ключей, связей и значений по умолчанию заданы проверки;
  • при сбое сохраняются xcresult и обезличенные журналы миграции;
  • удаление версии модели, переименование поля или изменение ограничений обязательно запускает план миграции;
  • полный тест выполняет прямое обновление с самой старой поддерживаемой версии до текущей.

Миграция базы данных — не одноразовый сценарий выпуска, а контракт совместимости, который развивается вместе с моделью. Зафиксировав исторические образцы, бизнес-инварианты и диагностические данные о сбоях, перед каждым слиянием можно отвечать на один и тот же вопрос: смогут ли существующие данные безопасно перейти на новую версию.

Часто задаваемые вопросы

Сколько старых версий базы нужно хранить?

Минимум нужны текущая опубликованная версия и предыдущая версия, с которой ещё возможен прямой переход. При длинном цикле обновления добавьте все поддерживаемые исходные версии.

Почему недостаточно скопировать основной файл SQLite?

В режиме WAL подтверждённые изменения могут оставаться в файле -wal. Сначала выполните безопасный checkpoint либо сохраняйте sqlite, sqlite-wal и sqlite-shm единым комплектом.

ZoomMini Mac в облаке

Выберите выделенный физический узел для сборки, тестирования и экспериментов

Две конфигурации M4 доступны в узлах Сингапура, Токио, Сеула и Гонконга. Ориентируйтесь на результат, который система вернёт при оформлении заказа.

Выбрать модель и заказать