Инженерный журнал BookaMac

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

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

Один и тот же код интерфейса iOS может нормально выглядеть на компьютере разработчика, но после слияния изменений привести к обрезанным кнопкам, смещению элементов из-за динамического размера шрифта или несоответствию цветов в тёмной теме. Проверка визуальной регрессии не заменяет модульные тесты. Её задача — при фиксированных условиях рендеринга ответить на конкретный вопрос: изменил ли текущий коммит пиксели, которые действительно видит пользователь?

Облачный Mac хорошо подходит для постоянного выполнения таких проверок. Однако для этого состояние симулятора, тестовые данные и правила сравнения необходимо хранить под управлением системы контроля версий. Нескольких сохранённых скриншотов, которые «выглядят правильно», недостаточно для создания стабильного контрольного этапа.

Сначала определите стабильную матрицу скриншотов

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

Параметр Рекомендуемое фиксированное значение Действия при изменении
Устройство Одна конкретная модель Simulator Создать отдельный каталог эталонов
Система Определённая доступная среда выполнения Повторно проверить эталонные изображения
Оформление Отдельные запуски для светлой и тёмной темы Не сравнивать разные темы между собой
Язык Отдельный скриншот для каждого целевого языка Добавлять код языка в имя файла
Размер шрифта Начать со стандартного размера Создать отдельную группу тестов для крупного шрифта
Данные Локальные фикстуры или тестовый интерфейс Не использовать случайное содержимое

Для эталонов можно использовать путь Snapshots/<runtime>/<device>/<locale>/<appearance>/. Такая структура каталогов получается немного длиннее, зато при сбое сразу видно, в каком окружении выполнялось сравнение. Это помогает не принять различия рендеринга между версиями системы за регрессию продукта.

Эталонное изображение не является навсегда зафиксированным правильным ответом. Это прошедший проверку контракт интерфейса, поэтому любое его обновление должно рассматриваться вместе с соответствующим изменением кода.

Закрепите симулятор вместо многократного использования текущего состояния

В симуляторе, который долго использовали вручную, остаются диалоги разрешений, настройки клавиатуры, кэш и тестовые учётные записи. Надёжнее выделить для визуальных тестов отдельное устройство и перед каждым запуском выключать его, очищать и загружать заново.

#!/usr/bin/env bash
set -euo pipefail

: "${DEVICE_UDID:?DEVICE_UDID is required}"

xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
xcrun simctl erase "$DEVICE_UDID"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b

xcrun simctl status_bar "$DEVICE_UDID" override \
  --time 09:41 \
  --batteryState charged \
  --batteryLevel 100 \
  --wifiBars 3 \
  --cellularBars 4

rm -rf TestResults/Visual.xcresult
xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme ExampleVisualTests \
  -destination "id=$DEVICE_UDID" \
  -resultBundlePath TestResults/Visual.xcresult

rm -rf TestResults/Attachments
xcrun xcresulttool export attachments \
  --path TestResults/Visual.xcresult \
  --output-path TestResults/Attachments

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

Создавайте скриншот при наступлении наблюдаемого состояния

Одна из самых распространённых ошибок в визуальных тестах — запускать приложение и всегда ждать две секунды. Из-за нагрузки на машину или изменений в работе сети две секунды могут оказаться то избыточными, то недостаточными. XCUITest должен ожидать доступный элемент, который показывает, что страница готова к сравнению.

func capture(_ name: String, readyIdentifier: String) {
    let app = XCUIApplication()
    app.launchArguments = ["-VisualTestMode", "1"]
    app.launch()

    let ready = app.descendants(matching: .any)[readyIdentifier]
    XCTAssertTrue(ready.waitForExistence(timeout: 15))

    let image = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: image)
    attachment.name = name
    attachment.lifetime = .keepAlways
    add(attachment)
}

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

Учитывайте шрифты и анимацию

Шрифты должны быть системными или поставляться вместе с приложением. Нельзя зависеть от их однократной ручной установки. Анимацию можно отключить с помощью аргументов запуска; перед созданием скриншота также нужно убедиться, что прокрутка остановилась. Для постоянно изменяющихся видео, карт и таймеров предпочтительны детерминированные тестовые фикстуры. Маски следует применять только к очень небольшим областям, которые невозможно контролировать.

Задайте объяснимые пороги сравнения пикселей

Различие байтов в PNG-файлах не означает, что изображения визуально отличаются. Прямое сравнение через cmp может реагировать на особенности кодирования. Инструмент сравнения должен сначала декодировать оба изображения до одинакового размера и пикселей в цветовом пространстве sRGB, а затем попиксельно вычислить различия каналов.

Рекомендуется одновременно использовать два условия:

  1. Максимальный допуск для отдельного канала, например разрешающий небольшие различия сглаживания.
  2. Долю пикселей, превышающих этот допуск: она не должна составлять больше крайне малой части всего изображения.

Если учитывать только среднее различие, можно пропустить серьёзную локальную проблему: исчезнувшая кнопка способна занимать лишь небольшую часть изображения. Инструмент сравнения также должен создавать изображение с выделенными различиями и записывать количество пикселей сверх порога, ограничивающую рамку и применённые пороговые значения. Для критически важных экранов подтверждения можно использовать строгие правила, а для страниц с тенями или сложными градиентами — отдельные настройки. Однако не следует постоянно ослаблять глобальные пороги только ради того, чтобы проверка снова стала «зелёной».

Превратите сбой в доступный для проверки набор доказательств

При каждом сбое необходимо архивировать как минимум эталонное изображение, фактическое изображение, изображение различий и .xcresult. Вместе с ними следует сохранять идентификатор коммита, версию Xcode, среду выполнения системы, модель устройства, язык и тему оформления. Имена файлов должны включать набор тестов, состояние страницы и окружение, чтобы параллельные задания не перезаписывали результаты друг друга.

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

Обновление эталонов также должно выполняться отдельным этапом: сначала создаются изображения-кандидаты, затем различия проверяются вручную, и только после подтверждения намеренного изменения эталоны заменяются. Тестовое задание не должно автоматически перезаписывать эталоны при сбое, иначе настоящая регрессия будет принята уже при следующем запуске.

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

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

Нужно ли требовать полного совпадения всех пикселей?

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

Почему один коммит даёт разные снимки в разных симуляторах?

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

Что сохранять после провала визуальной проверки?

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

Выделенный физический Mac mini

Перенесите проверенный рабочий процесс в облачный Mac, работающий онлайн 365 дней в году

Выберите BookaMac M4 или BookaMac M4 Pro под свои задачи и при оформлении заказа проверьте регион, срок аренды и дополнительные варианты хранилища.

Выбрать вариант аренды