Пользователь редактирует неотправленную форму на облачном Mac, переключается в другое приложение, чтобы свериться с данными, а через несколько минут система завершает процесс. При следующем запуске худший результат — не сбой приложения, а внешне нормальный интерфейс, в котором черновик исчез, навигация вернулась на главный экран или восстановился уже недействительный объект. Такие сценарии сложно охватить обычными модульными тестами. Для них лучше воспроизводить полный жизненный цикл с помощью XCUITest в изолированном симуляторе.
Сначала определите, какие состояния должны восстанавливаться
Не рассматривайте «восстановление предыдущего экрана» как единое требование. Прежде чем писать тесты, разделите состояние на три категории:
| Тип состояния | Примеры | Ожидаемое поведение после повторного запуска |
|---|---|---|
| Бизнес-состояние | Текст черновика, условия фильтрации, выбранный элемент | Восстановить по правилам продукта |
| Состояние навигации | Текущий экран, вкладка, позиция в списке | Восстановить до уровня, который по-прежнему действителен |
| Временное состояние | Индикатор загрузки, диалоговое окно, одноразовый токен | Отбросить и вычислить заново |
Логика восстановления также должна различать обычный переход в фон и аварийное завершение. В первом случае можно сохранить больше деталей интерфейса, а во втором приоритетом должна быть согласованность данных. Если черновик связан с уже удалённым объектом, приложение должно перейти на безопасный экран и объяснить причину, а не пытаться принудительно воссоздать недействительный интерфейс.
Цель тестирования восстановления состояния — не вернуть каждый пиксель на прежнее место, а убедиться, что пользователь может продолжить работу и не увидит устаревшие, недоступные по правам или не подлежащие отправке данные.
Для каждого сценария рекомендуется зафиксировать четыре пункта: исходное состояние, действие завершения, проверки после восстановления и состояния, которые не должны возникнуть. Тогда при сбое теста команда сможет понять, где находится проблема: в сохранении данных, маршрутизации или их проверке.
Подготовьте детерминированную точку входа для автоматизации
UI-тесты не должны зависеть от данных, оставшихся после предыдущего запуска. Приложение может импортировать фиксированную тестовую фикстуру при запуске со специальными аргументами и записывать данные в отдельный контейнер. Название фикстуры должно описывать бизнес-сценарий, а не использовать первичный ключ базы данных, который может измениться.
let arguments = ProcessInfo.processInfo.arguments
if arguments.contains("-ui-testing"),
let index = arguments.firstIndex(of: "-fixture"),
arguments.indices.contains(index + 1) {
let fixture = arguments[index + 1]
try TestFixtureLoader.load(named: fixture)
}
Рабочая сборка должна игнорировать или полностью исключать эту тестовую точку входа. Более надёжный вариант — использовать условную компиляцию и включать загрузчик только во внутренних тестовых конфигурациях. Перед началом теста также следует очищать уведомления, кеши и общие настройки, чтобы случайно сохранившийся старый черновик не привёл к ложноположительному результату.
Даже при выполнении тестов на выделенных физических узлах Runner каждой линии CI следует назначать отдельный набор устройств симулятора. Выделенные вычислительные ресурсы не означают автоматическую изоляцию тестовых данных: два параллельных задания, использующих один симулятор, всё равно могут перезаписывать контейнеры приложений друг друга.
Воспроизводите завершение и повторный запуск процесса с помощью XCUITest
Следующий тест сначала создаёт черновик, затем переводит приложение в фон, завершает процесс и запускает его заново. Всем ключевым элементам управления следует назначить стабильные accessibility identifier, чтобы поиск элементов не зависел от локализованного текста.
func testDraftRestoresAfterTermination() {
let app = XCUIApplication()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project",
"-reset-state", "YES"
]
app.launch()
app.buttons["project.create"].tap()
let editor = app.textViews["draft.editor"]
editor.tap()
editor.typeText("release checklist")
app.buttons["draft.save"].tap()
XCUIDevice.shared.press(.home)
app.terminate()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project"
]
app.launch()
XCTAssertTrue(app.navigationBars["draft.screen"].waitForExistence(timeout: 8))
XCTAssertEqual(app.textViews["draft.editor"].value as? String,
"release checklist")
XCTAssertTrue(app.staticTexts["restoration.completed"].exists)
}
Не передавайте аргумент сброса при втором запуске в том же тесте: иначе тест сам удалит состояние, которое должен проверить. Также недостаточно убедиться лишь в наличии редактора. Одновременно проверяйте содержимое, заголовок навигации, выбранный объект и уведомление о завершённом восстановлении.
Тестируйте три пути запуска отдельно
Сохраните как минимум три независимых теста:
- Возврат из фона непосредственно на передний план: проверка того, что после короткого перерыва весь экран не создаётся заново.
- Завершение процесса после сохранения состояния: проверка возможности продолжить работу после повторного запуска.
- Повторный запуск с недействительным объектом: проверка безопасного перехода приложения в резервное состояние и удаления недействительного маршрута.
Каждый тест должен проверять только один вариант жизненного цикла. Так найти причину сбоя будет проще, чем в длинном тесте из десятка и более шагов.
Изолируйте симулятор и рабочие каталоги на облачном Mac
Задание автоматизации должно явно создавать временный набор устройств, а не использовать симуляторы из стандартного каталога текущего пользователя. Удаление набора устройств после завершения задания одновременно очищает контейнеры приложений и оставшиеся снимки состояния.
set -euo pipefail
DEVICE_SET="$RUNNER_TEMP/CoreSimulator"
RESULTS="$RUNNER_TEMP/StateRestoration.xcresult"
mkdir -p "$DEVICE_SET"
xcrun simctl --set "$DEVICE_SET" create \
"State-Restore-iPhone" \
"com.apple.CoreSimulator.SimDeviceType.iPhone-16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0"
UDID="$(xcrun simctl --set "$DEVICE_SET" list devices \
available -j | /usr/bin/python3 scripts/first_device.py)"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleUITests \
-destination "platform=iOS Simulator,id=$UDID" \
-resultBundlePath "$RESULTS"
xcrun simctl --set "$DEVICE_SET" shutdown "$UDID" || true
rm -rf "$DEVICE_SET"
Указанные в примере среду выполнения и тип устройства необходимо заменить версиями, установленными на узле. Сначала их можно проверить командами xcrun simctl list runtimes и xcrun simctl list devicetypes. Не позволяйте скрипту автоматически и без уведомления выбирать «новейшую» среду выполнения. После обновления Xcode одновременно могут измениться снимки экрана, системные диалоги и поведение восстановления.
Превратите данные о сбое в диагностируемый результат
Сбой восстановления состояния обычно проявляется после второго запуска, поэтому важно сохранять данные как до, так и после восстановления. Рекомендуется прикладывать снимки экрана после ключевых действий, а также добавлять в XCTest attachment название использованной фикстуры, идентификатор симулятора, версию приложения и этап восстановления. При сбое сохраняйте .xcresult; для успешных заданий срок хранения можно сократить в соответствии с правилами команды.
Используйте фиксированный порядок диагностики:
- Убедитесь, что при первом запуске состояние действительно было записано.
- Проверьте, завершилось ли атомарное сохранение при переходе в фон.
- Убедитесь, что при втором запуске логика очистки не выполнилась повторно.
- Проверьте успешность миграции версии сохранённых данных.
- В последнюю очередь проверьте, принимает ли маршрутизация восстановленный объект.
Если черновик восстановлен, но приложение вернулось на главный экран, проблема, скорее всего, связана с реконструкцией навигации. Если открыт правильный экран, но поля пусты, проверьте момент сохранения и ключи хранилища. Если сбои возникают только при параллельном выполнении, сначала убедитесь, что симуляторы, каталоги результатов и фикстуры действительно изолированы.
После включения этих сценариев в обязательные проверки перед слиянием восстановление состояния больше не будет зависеть от ручного многократного переключения между приложениями. При каждом изменении модели хранения данных, жизненного цикла Scene или структуры навигации линия CI сможет на одинаковых входных данных проверить, может ли пользователь по-прежнему продолжить работу с места, где она была прервана.
Часто задаваемые вопросы
Можно ли запускать такие тесты в повседневном симуляторе разработчика?
Не следует. Для автоматизации нужен отдельный симулятор или набор устройств, а перед каждым независимым сценарием данные приложения необходимо приводить к известному состоянию.
Почему недостаточно проверить только успешный повторный запуск?
Успешный запуск подтверждает лишь отсутствие немедленного сбоя. Нужно отдельно проверить текст черновика, уровень навигации, выбранный объект, сообщение о восстановлении и удаление чувствительных временных данных.
Запустите следующую задачу на облачном M4 Mac
Выберите Runner M4 или Runner M4 Plus и настройте узел, срок аренды и дополнительные параметры хранилища под свой проект. Каждый заказ включает выделенную физическую машину, а не виртуальную.