После переноса набора модульных тестов на облачный Mac команда легко может сосредоточиться только на том, прошли ли тесты. Однако в одном из коммитов могут удалить утверждения для важной ветви: все тесты останутся зелёными, а покрытие незаметно снизится. Надёжнее создавать отдельный xcresult для каждой сборки, извлекать данные встроенной в Xcode утилитой xccov и одновременно проверять общее покрытие проекта и файлы, изменённые в текущем коммите.
Зафиксируйте входные условия покрытия
Сравнивать покрытие имеет смысл только при стабильных условиях запуска тестов. Зафиксируйте версию Xcode, scheme, план тестирования, модель симулятора и версию системы; не позволяйте разным исполнителям самостоятельно выбирать destination. В команде запуска тестов также следует явно включить сбор покрытия и создавать новый каталог результатов для каждого запуска.
set -euo pipefail
RESULT_DIR="$PWD/Artifacts/Coverage"
RESULT_BUNDLE="$RESULT_DIR/TestResults.xcresult"
rm -rf "$RESULT_BUNDLE"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace ExampleApp.xcworkspace \
-scheme ExampleApp-CI \
-testPlan UnitTests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-enableCodeCoverage YES \
-resultBundlePath "$RESULT_BUNDLE"
Путь, указанный в resultBundlePath, не должен существовать до запуска команды, иначе старые результаты могут помешать записи. CI-скрипт также должен сохранять вывод xcodebuild -version, идентификатор текущего коммита и полный destination, чтобы изменение инструментария не приняли за регрессию в коде.
Пороговая проверка покрытия оценивает изменения при одинаковых условиях тестирования, а не сравнивает результаты разных симуляторов или планов тестирования.
Извлекайте из xcresult данные, пригодные для аудита
После завершения тестов экспортируйте JSON с помощью xccov, а не разбирайте форматированный текст из терминала. JSON надёжнее сохраняет пути к файлам, имена целей и построчное покрытие, а его столбцы не смещаются при изменении ширины вывода.
xcrun xccov view \
--report \
--json \
"$RESULT_BUNDLE" > "$RESULT_DIR/coverage.json"
test -s "$RESULT_DIR/coverage.json"
Парсер должен сначала убедиться в наличии targets, а затем агрегировать файлы по целям. Если отчёт пуст, не следует считать его результатом с покрытием 0% и продолжать сравнение — такой случай нужно сразу помечать как сбой сбора данных. Среди распространённых причин: тестирование не включено в scheme, цель не участвует в сборе покрытия либо процесс тестирования аварийно завершился до записи отчёта.
Сохраняйте исходные подтверждающие данные
Рекомендуется сохранять следующие материалы как артефакты одного и того же задания:
| Артефакт | Назначение |
|---|---|
TestResults.xcresult |
Проверка тестов, журналов и источника данных о покрытии |
coverage.json |
Стабильный разбор скриптами |
coverage-summary.json |
Хранение порогов, фактических значений и списка проблемных файлов |
command.txt |
Восстановление параметров запуска и версии инструментария |
Скриншот с процентами не позволяет выяснить, какие тесты действительно выполнялись. Основой для диагностики служит исходный пакет результатов.
Настройте два уровня пороговой проверки
Общее покрытие проекта помогает выявлять значительные падения, но при добавлении нескольких десятков строк непокрытого кода в большой кодовой базе итоговое значение может измениться лишь на несколько десятых процентного пункта. Поэтому рекомендуется использовать два правила:
- Общее покрытие проекта не должно опускаться ниже фиксированного минимума.
- Изменённые в текущем коммите исходные файлы с исполняемым кодом должны соответствовать более высокому порогу.
Например, минимальное общее покрытие можно установить на уровне 72%, а порог для изменённых файлов — на уровне 85%. Пороговые значения должны основываться на текущем базовом уровне команды, а не на идеальном показателе, который невозможно сразу обеспечить. Следующий фрагмент Python показывает, как получить покрытие целевой сборки; в реальном проекте можно дополнительно развернуть files и проверять каждый файл отдельно.
import json
import sys
with open("Artifacts/Coverage/coverage.json", encoding="utf-8") as f:
report = json.load(f)
targets = report.get("targets", [])
if not targets:
raise SystemExit("Coverage report has no targets")
tested = [t for t in targets if t.get("name") == "ExampleApp.app"]
if len(tested) != 1:
raise SystemExit("Expected application target was not found")
coverage = float(tested[0]["lineCoverage"]) * 100
minimum = 72.0
print(f"application_line_coverage={coverage:.2f}")
sys.exit(0 if coverage >= minimum else 2)
Скрипт должен использовать разные коды завершения для случаев, когда покрытие ниже порога и когда отчёт невозможно разобрать. В первом случае нужно добавить тесты или обосновать изменение, а во втором — исправить цепочку сбора данных. Объединять эти ситуации в один тип ошибки нельзя.
Проверяйте только файлы, которым действительно нужны тесты
Список изменённых файлов можно получить с помощью git diff --name-only, а затем пересечь его с путями из отчёта xccov. Не включайте в пороговую проверку все файлы .swift без разбора. Обычно следует явно исключить:
- автоматически сгенерированные средства доступа к ресурсам и клиенты API;
- вспомогательный тестовый код из каталога
Tests; - файлы моделей, содержащие только объявления и не имеющие исполняемой логики;
- исходный код, созданный инструментами сборки и не предназначенный для ручной поддержки.
Правила исключения должны храниться в репозитории и проходить ревью кода. Нельзя добавлять временную маску уже после сбоя проверки. Перед сравнением путей также нужно привести их к корню репозитория, разрешить символические ссылки и удалить префиксы временных каталогов сборки, иначе один и тот же файл может не совпасть из-за различий в абсолютных путях.
Для переименованных файлов git diff --name-status -M надёжнее простого списка файлов. Если файл перемещён со старого пути на новый, соответствующую запись отчёта следует искать по новому пути, а не считать файл отсутствующим в отчёте.
Контролируйте параллельное выполнение тестов и колебания результатов
Случайные изменения покрытия обычно вызваны не самим xccov, а недетерминированным выполнением тестов. Асинхронные тесты должны ожидать наступления определённого состояния, а не использовать паузу фиксированной продолжительности. Общие базы данных, синглтоны и временные каталоги необходимо сбрасывать перед каждым тестом. Если параллельные тесты конкурируют за один ресурс, сначала отключите параллельное выполнение для соответствующих тестовых целей, а затем найдите конкретную общую точку.
Жизненный цикл симулятора также должен быть отслеживаемым. На постоянно работающих исполнителях перед заданием можно очищать данные приложения, но удалять все среды выполнения при каждом запуске необязательно. Важно проверить, даёт ли один и тот же план тестирования стабильные результаты при неизменном инструментарии. Для начала можно трижды выполнить тесты на одном коммите и записать разницу по каждому файлу. Для файлов с постоянными колебаниями сначала следует исправить изоляцию тестов и только потом включать их в строгую пороговую проверку.
Наконец, сформируйте сводку ошибки так, чтобы разработчик мог сразу действовать: укажите фактическое общее покрытие, требуемое значение, изменённые файлы ниже порога, число покрытых и исполняемых строк в каждом таком файле, а также расположение архива с исходным xcresult. Тогда разработчику не придётся повторно запускать всё задание, чтобы понять, нужно ли добавить тесты, исправить скрипт или устранить нестабильность тестовой среды.
Часто задаваемые вопросы
Достаточно ли проверять только общее покрытие проекта?
Нет. Общий показатель обнаруживает крупный откат, но почти не реагирует на небольшой новый модуль. Лучше одновременно проверять общий минимум и покрытие изменённых файлов, исключив генерируемый и вспомогательный код.
Почему покрытие одного и того же коммита иногда различается?
Причиной бывают разные наборы тестов, незавершённые асинхронные операции, общее состояние параллельных тестов, данные симулятора и версия Xcode. Сначала зафиксируйте scheme, destination, план тестов и инструменты.
Какие файлы сохранять после провала проверки?
Сохраните исходный xcresult, coverage.json, итог сравнения с порогами, точную команду запуска и идентификатор коммита. Этого достаточно, чтобы отличить отсутствие тестов от ошибки разбора или реального снижения.
Воспроизводите рабочие процессы на облачном Mac, включая и выключая его по мере необходимости
Выберите подходящую конфигурацию Apple Silicon из трёх вариантов и арендуйте ресурс на день, неделю, месяц или квартал. Вы получаете выделенную физическую машину, а не виртуальную; актуальный статус доступности отображается в консоли в реальном времени.