Команда «Записать прочитанное» находится на машине разработчика, но не появляется в симуляторе на удалённой сборочной машине. Не спешите менять код приложения: корректность параметров, выполнение Intent и обнаружение действия системой — три разные проверки. Облачный Mac помогает закрепить тестовую среду для первых двух; последнюю всё равно нужно проверить в симуляторе с установленным приложением.
Сначала определите границы автоматического тестирования
Для проверки процесса возьмём пример действия без записи на диск: оно принимает идентификатор книги, удаляет пробелы в начале и конце и возвращает нормализованный идентификатор. Так мы сначала исключим влияние разрешений на хранилище и нестабильной сети. Когда появится работа с базой данных, запись и поведение при повторном выполнении нужно будет тестировать отдельно: успешный ответ ещё не означает, что данные сохранены.
Приёмка состоит из трёх уровней: чистые тесты Swift проверяют пустой ввод и недопустимые символы; XCTest вызывает perform() и проверяет результат и ошибки; при ручной проверке в симуляторе убеждаются, что действие видно, параметр можно заполнить, а результат — прочитать. Сбои на первых двух уровнях должны блокировать слияние изменений. Третий уровень зависит от системной индексации, поэтому его лучше включить в проверку перед выпуском версии, а не писать тест с фиксированным временем ожидания.
Не удаляйте и не устанавливайте приложение заново раз за разом в автоматическом тесте только ради появления команды. Сначала подтвердите, что контракт кода соблюдён, а затем проверяйте установку, запуск приложения и обнаружение действия системой.
Вынесите правила ввода в тип, который можно тестировать напрямую
Правила ниже допускают от 1 до 32 символов: латинские буквы ASCII, цифры, подчёркивания и дефисы. Пробельные символы допустимы только в начале и конце ввода; при других символах возвращается явная ошибка. В примере perform() лишь возвращает идентификатор и не сохраняет данные.
import AppIntents
import Foundation
enum ReadingIDError: LocalizedError {
case invalid
var errorDescription: String? {
"编号须为 1–32 位字母、数字、下划线或连字符"
}
}
enum ReadingID {
static func normalize(_ raw: String) throws -> String {
let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let pattern = #"^[A-Za-z0-9_-]{1,32}$"#
guard value.range(of: pattern, options: .regularExpression) != nil else {
throw ReadingIDError.invalid
}
return value
}
}
struct CheckReadingIDIntent: AppIntent {
static var title: LocalizedStringResource = "检查书籍编号"
static var openAppWhenRun = false
@Parameter(title: "书籍编号")
var bookID: String
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let normalized = try ReadingID.normalize(bookID)
return .result(value: normalized)
}
}
Добавьте название параметра, название действия и текст ошибки в ресурсы локализации проекта. Китайские строки здесь служат примером и не должны оставаться единственным вариантом текста в многоязычном приложении. Сами правила принимают только ASCII-идентификаторы. Если по требованиям допустимы иероглифы или символы других письменностей, сначала определите разрешённый набор символов, а затем измените регулярное выражение и тестовые данные.
Проверяйте граничные случаи, а не только корректный ввод
Как минимум проверьте, что " book_7 " превращается в "book_7", а пустая строка, пробелы внутри идентификатора и идентификаторы длиннее 32 символов отклоняются. В ветке ошибки проверяйте её тип, а не просто факт возникновения исключения: иначе тест может пройти и при неожиданной ошибке нижележащей зависимости. При тестировании perform() также убедитесь, что результат соответствует правилам нормализации. Разделяйте тесты чистой логики и тесты вызова Intent, чтобы при сбое понимать, изменились правила ввода или обёртка системного интерфейса.
Зарегистрируйте действие и проверьте его в симуляторе
Чтобы система могла показывать предлагаемое действие, добавьте AppShortcutsProvider в цель приложения. В фразе сохраните заполнитель имени приложения: он помогает системе связать действие с текущим приложением. Это не строка для shell.
import AppIntents
struct ReadingShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CheckReadingIDIntent(),
phrases: ["在 \(.applicationName) 检查书籍编号"],
shortTitle: "检查书籍编号",
systemImageName: "book"
)
}
}
Сначала убедитесь, что файл входит в цель приложения, а не только в тестовую цель. Установите собранное приложение и запустите его один раз. Затем откройте «Команды» в симуляторе, найдите действие по названию, введите допустимый идентификатор и выполните его. После этого отправьте недопустимый идентификатор и проверьте, понятен ли текст ошибки. Если действие не появилось, запишите версию системы, версию сборки приложения и последовательность действий после установки. Не выдавайте случайное появление действия после повторного открытия экрана за надёжный способ исправления.
Зафиксируйте тестовое устройство и каталог результатов
На облачном Mac от ZSHMac сначала выполните xcrun simctl list devices available, чтобы увидеть установленные доступные симуляторы, выберите UDID устройства и задайте SIM_UDID. Команда ниже предполагает, что в проекте есть общая схема Reader; при запуске используйте схему своего проекта. Каждый раз указывайте новый путь для пакета результатов, иначе существующий файл может привести к немедленному сбою команды тестирования.
mkdir -p artifacts
test -n "${SIM_UDID:-}" || { printf '%s\n' 'SIM_UDID is required' >&2; exit 1; }
result="$PWD/artifacts/app-intents-$(date +%Y%m%d-%H%M%S)-$$.xcresult"
xcodebuild test \
-scheme Reader \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath "$result"
В CI сохраняйте код завершения команды и пакет результатов: код определяет, прошла ли задача, а пакет помогает найти тест, вызвавший сбой. Если схема не является общей или нужный симулятор недоступен, сначала исправьте условия запуска, а не относите проблему к логике Intent. При ручной приёмке записывайте, появилось ли действие, корректна ли подсказка для ввода и как оно ведёт себя с допустимым и недопустимым параметром.
При сбое проверяйте каждый уровень отдельно
| Симптом | Сначала проверьте | Не начинайте с |
|---|---|---|
| Не проходит тест чистой логики | Диапазон символов, длину и обработку пробелов по краям | Переустановки симулятора |
Результат perform() не соответствует ожидаемому |
Передачу параметра и тип возвращаемого значения | Ожидания системной индексации |
| Команда тестирования не находит устройство | Список доступных устройств и UDID | Изменения правил Swift |
| Действия нет в «Командах» | Принадлежность файла цели приложения, установку и первый запуск | Добавления фиксированной задержки в тест |
| Действие есть, но при выполнении возникает ошибка | Подсказку для параметра, тип ошибки и версию целевой системы | Подавления всех ошибок |
Цель такой поэтапной проверки — сузить область поиска неисправности, а не гарантировать, что системное действие станет видно сразу после установки. Приёмку можно считать завершённой, когда тесты кода прошли, а действие в симуляторе проверено с допустимым и недопустимым вводом. После подключения реальной записи понадобятся отдельные тесты повторного выполнения и сохранения данных на диск: критерий успешности из этого примера, где возвращается только идентификатор, уже не подойдёт.
Часто задаваемые вопросы
Почему действия нет в Командах, если XCTest прошёл?
Убедитесь, что приложение с AppShortcutsProvider установлено и запущено. Проверьте версию системы и настройки цели; индексация действий может происходить с задержкой.
Где разместить проверку параметров?
Вынесите повторно используемые правила в отдельный тип Swift и вызывайте его из perform(). Тогда модульные тесты не зависят от системной индексации.
Достаточно ли CI без графического интерфейса?
Нет. CI проверяет входные данные и результат, а видимость действия и запрос параметров нужно проверить в симуляторе или на устройстве с целевой версией системы.
Нужен выделенный Mac в облаке?
Выберите модель, локацию и срок оплаты. Вам будет предоставлен выделенный физический сервер; фактическая доступность отображается в консоли в реальном времени.