iOS-приложение сохраняет офлайн-черновики в SQLite. Пользователь видит сообщение «Сохранено», но при следующей записи процесс завершается с ошибкой. Останется ли черновик после повторного запуска? На облачном Mac с отдельным симулятором это можно проверить воспроизводимым экспериментом: сначала подтвердить транзакцию с контрольной записью, затем вызвать сбой до подтверждения второй транзакции и напрямую проверить базу данных. Здесь проверяется граница транзакции, а не скорость сборки. Сам по себе повторный запуск приложения не заменяет проверку данных.
Сначала определите ожидаемый результат
Подготовьте тестовую таблицу всего с двумя столбцами: id и phase. Запись 1|committed должна быть подтверждена до сбоя. Запись 2|pending нужно внести в новую транзакцию, не подтверждая её. После сбоя проверка целостности должна вернуть ok, а запрос — только первую запись. Если присутствует и вторая, сначала проверьте, не расположен ли вызов сбоя после подтверждения транзакции. Если исчезла первая, убедитесь, что она действительно была записана в тот же файл базы данных.
WAL не гарантирует, что «ни одна запись никогда не потеряется». Этот тест проверяет только то, что успешно подтверждённая транзакция остаётся доступной для чтения после данного сбоя процесса, а неподтверждённая запись не становится сохранёнными данными. Он не заменяет испытания на отключение питания, повреждение хранилища или сбои синхронизации на уровне приложения.
Используйте одноразовый симулятор и тестовый идентификатор пакета. Заранее зафиксируйте версию Xcode и среду выполнения симулятора, запишите вывод xcodebuild -version и идентификатор симулятора; каждый прогон выполняйте в одинаковых условиях. Не внедряйте сбой в личном симуляторе для разработки или в базе данных реальных пользователей.
Внедрите сбой на границе транзакции
Оставьте точку внедрения сбоя только в отладочной сборке и требуйте для её запуска явный аргумент, чтобы она не срабатывала при обычном использовании. Функция ниже предполагает, что вызывающий код уже открыл Documents/Recovery.sqlite, а результат каждого вызова SQL проверяется. В примере precondition немедленно выявляет ошибку подготовки, вместо того чтобы продолжать тест и получать вводящие в заблуждение результаты запроса.
#if DEBUG
import Darwin
import SQLite3
func runRecoveryProbe(_ db: OpaquePointer) {
func execute(_ sql: String) {
precondition(sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK)
}
execute("PRAGMA journal_mode=WAL")
execute("CREATE TABLE IF NOT EXISTS probe (id INTEGER PRIMARY KEY, phase TEXT NOT NULL)")
execute("INSERT OR REPLACE INTO probe VALUES (1, 'committed')")
execute("BEGIN IMMEDIATE")
execute("INSERT OR REPLACE INTO probe VALUES (2, 'pending')")
abort()
}
#endif
Вызывайте функцию только при наличии специального тестового маркера в аргументах запуска. Тестовый код должен предварительно убедиться, что используется экспериментальная база данных и в ней нет ранее открытой незавершённой транзакции. PRAGMA journal_mode=WAL нужно выполнять вне транзакции. abort() аварийно завершает процесс в этом прогоне, поэтому после него намеренно нет COMMIT. Если в проекте используется обёртка для работы с базой данных, создайте ту же границу транзакции через неё. Не обходите обёртку, утверждая затем, что проверили рабочий путь записи.
Не допускайте влияния старых записей
Перед каждым прогоном сбрасывайте данные тестового приложения в одноразовом симуляторе либо устанавливайте его в новый выделенный симулятор. Недостаточно выполнить только DELETE FROM probe, оставив другие таблицы и файлы приложения. Перед первым запуском убедитесь, что база данных действительно находится в контейнере данных приложения. До и после внедрения сбоя записывайте путь к базе данных, версию сборки и идентификатор симулятора, но не копируйте реальные данные проекта в экспериментальный контейнер.
После сбоя проверьте базу данных на месте
Запустите приложение и, прежде чем выполнять запрос, убедитесь, что оно аварийно завершилось из-за заданного сбоя. Сам по себе возврат команды запуска этого не доказывает: приложение могло ещё не дойти до тестового кода. Сопоставьте состояние процесса в симуляторе с отчётом о сбое. Прогон без подтверждения сбоя следует считать «незавершённым тестом», а не успешным.
Убедившись, что процесс завершён, откройте контейнер данных приложения из терминала облачного Mac. Подставьте идентификатор симулятора текущего прогона и тестовый идентификатор пакета:
UDID="your-simulator-udid"
BUNDLE_ID="com.example.RecoveryProbe"
DATA="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)"
sqlite3 "$DATA/Documents/Recovery.sqlite" \
'PRAGMA integrity_check; SELECT id, phase FROM probe ORDER BY id;'
Ожидаемый вывод — последовательно ok и 1|committed. Перед проверкой не удаляйте файлы -wal и -shm рядом с базой данных и не переносите их отдельно: открывайте базу через SQLite в исходном состоянии. Во время проверки не запускайте приложение, которое может снова записать данные. Если проект использует несколько баз данных, сначала сверьте имя файла и путь к контейнеру, чтобы не принять проверку не того файла за ошибку восстановления.
Разделите ошибки по возможным причинам
Если вывод содержит 2|pending, в первую очередь проверьте, не произошло ли автоматическое подтверждение транзакции, не завершила ли её обёртка раньше времени и не направил ли маркер сбоя выполнение по другой ветке кода. Если отсутствует 1|committed, проверьте успешность первой записи и убедитесь, что тест выполнялся через то же соединение с тем же файлом. Если запрос сообщает, что таблицы нет, сначала проверьте, установлена ли сборка с тестовой точкой входа и не указывает ли путь к контейнеру на симулятор из предыдущего прогона.
integrity_check не проверяет бизнес-условие: даже при результате ok записи необходимо сверить по отдельности. И наоборот, отчёт о сбое доказывает лишь аварийное завершение процесса, но не правильность восстановления транзакций. Сохраняйте оба вида подтверждений вместе с версией Xcode, идентификатором симулятора и идентификатором тестовой сборки — тогда при повторении ошибки условия можно будет сопоставить.
Сохраните границы теста в CI
Выделите тесту отдельный симулятор. После внедрения сбоя и подтверждения завершения процесса запускайте отдельный шаг проверки без записи в базу данных. Не используйте один контейнер данных приложения совместно с обычными UI-тестами: параллельная запись изменит результат наблюдения. Точка внедрения сбоя должна существовать только в отладочной сборке и отсутствовать в релизной; CI должен проверять это явно.
В конце проведите контрольный прогон без внедрения сбоя: подтвердите вторую запись обычным способом и убедитесь, что её можно прочитать. Так вы проверите не только откат неподтверждённой записи, но и то, что сам тест не блокирует любые записи. Только обе группы результатов превращают случайное наблюдение после перезапуска в воспроизводимую проверку сохранности данных.
Часто задаваемые вопросы
Можно ли заменить сбой обычным завершением приложения?
Нет. При обычном завершении транзакция может подтвердиться или выполнится очистка. Нужно прервать процесс до подтверждения и зафиксировать факт сбоя.
Нужно ли удалять файлы -wal и -shm перед проверкой?
Нет. Сохраните сопутствующие файлы и откройте базу через SQLite для проверки целостности и записей.
Перенесите следующую сборку Xcode на выделенный физический сервер.
Выберите модель Apple Silicon, регион и срок аренды, а затем запускайте собственный процесс сборки через удалённое подключение. Фактическая доступность отображается в консоли при оформлении заказа.