Инженерная статья Runner

Воспроизводимые тесты буфера обмена iOS через simctl

Воспроизводимые тесты буфера обмена iOS через simctl

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

Сначала определите границы тестирования

Этот процесс подходит для проверки обычного текста, Unicode, пробельных символов и обработки переносов строк. Не следует использовать pbcopy как средство внедрения произвольных двоичных данных. Изображения, файловые URL и пользовательские UTI нужно предоставлять через тестовые фикстуры внутри приложения или специализированный тестовый хост.

Сначала перечислите поведение, которое действительно необходимо защитить:

Цель автоматизации — не прямое чтение системного буфера обмена, а проверка того, что приложение показывает правильный результат после инициированного пользователем копирования или вставки.

Подготавливайте чистый симулятор для каждой задачи

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

set -euo pipefail

: "${IOS_UDID:?IOS_UDID is required}"
JOB_ROOT="${PWD}/artifacts/${CI_JOB_ID:-local}"
mkdir -p "$JOB_ROOT"

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

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

Объект изоляции Рекомендуемый подход Типичная проблема при совместном использовании
Симулятор Один UDID на задачу Буфер обмена и состояние приложения перезаписываются
DerivedData Отдельный каталог для каждой задачи Параллельная запись делает результаты сборки нестабильными
Пакет результатов Новый путь для каждого запуска Старые вложения попадают в отчёт о текущем сбое
Текстовые фикстуры Хранить в системе контроля версий Входные данные меняются без изменений в тестовом коде

Внедряйте проверяемые текстовые фикстуры

Не собирайте сложные строки Unicode непосредственно в Shell. Храните фикстуры в файлах UTF-8, чтобы при проверке кода было видно, как именно изменились входные данные.

mkdir -p Tests/ClipboardFixtures

printf '第一行\n第二行:café\nemoji:🧪\n' \
  > Tests/ClipboardFixtures/multiline.txt

xcrun simctl pbcopy "$IOS_UDID" \
  < Tests/ClipboardFixtures/multiline.txt

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

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

xcrun simctl pbpaste "$IOS_UDID" > "$JOB_ROOT/clipboard-before.txt"
cmp Tests/ClipboardFixtures/multiline.txt \
  "$JOB_ROOT/clipboard-before.txt"

В рабочем CI-конвейере исходный текст не нужно хранить длительное время. После подтверждения совпадения удалите файл либо сохраните только результат shasum -a 256.

Запускайте вставку и копирование через реальные точки входа

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

func testPasteMultilineFixture() {
    let app = XCUIApplication()
    app.launchArguments += ["-ui-testing"]
    app.launch()

    let pasteButton = app.buttons["clipboard.paste"]
    XCTAssertTrue(pasteButton.waitForExistence(timeout: 10))
    pasteButton.tap()

    let editor = app.textViews["clipboard.editor"]
    XCTAssertTrue(editor.waitForExistence(timeout: 5))
    XCTAssertEqual(
        editor.value as? String,
        "第一行\n第二行:café\nemoji:🧪"
    )
}

Если система показывает запрос на подтверждение вставки, тест должен рассматривать его как часть взаимодействия, а не обходить через внутренний API. Идентификаторы accessibility элементов управления должны быть стабильными и не зависеть от локализованного текста кнопок.

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

Запуск, сбор доказательств и диагностика

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

xcodebuild build-for-testing \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData"

xcodebuild test-without-building \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData" \
  -resultBundlePath "$JOB_ROOT/ClipboardTests.xcresult"

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

Для задачи можно настроить очистку при выходе, сохраняя при этом xcresult, журналы симулятора и снимки экрана в случае сбоя. Исходное содержимое буфера обмена по умолчанию следует удалять, оставляя только длину, кодировку, хеш и имя фикстуры. Если тест затрагивает данные, которые мог ввести пользователь, журналы необходимо обезличить до архивирования.

Закрепите проверки в CI-конвейере

Стабильная задача регрессионного тестирования буфера обмена должна как минимум соответствовать следующим условиям:

  1. UDID, DerivedData и каталог результатов изолированы для каждой задачи;
  2. версия среды выполнения симулятора записывается;
  3. текстовые фикстуры хранятся в системе контроля версий как файлы UTF-8;
  4. тест запускает вставку или копирование через действие пользователя;
  5. для пустого буфера обмена, Unicode и переносов строк предусмотрены отдельные сценарии;
  6. при сбое сохраняются данные интерфейса, но исходное содержимое буфера обмена не хранится длительное время;
  7. после завершения задачи временное состояние симулятора очищается.

Ценность таких тестов заключается не в проверке самого системного буфера обмена, а в преобразовании контракта между приложением и буфером обмена в воспроизводимые входные и выходные данные для приёмочного тестирования. Когда каждый сбой можно сопоставить с конкретным UDID, хешем фикстуры, последовательностью действий и пакетом результатов, удалённая диагностика больше не зависит от пересказа вручную.

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

Подходит ли simctl pbcopy для тестирования изображений?

Не как универсальный способ загрузки двоичных данных. Текст удобно проверять через pbcopy и pbpaste, а изображения, файловые URL и собственные UTI следует передавать через тестовые данные приложения или отдельный тестовый хост.

Можно ли параллельным задачам использовать один симулятор iOS?

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

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

Запустите следующую задачу на облачном M4 Mac

Выберите Runner M4 или Runner M4 Plus и настройте узел, срок аренды и дополнительные параметры хранилища под свой проект. Каждый заказ включает выделенную физическую машину, а не виртуальную.

Арендовать облачный Mac