В конечном автомате встроенных покупок сложнее всего воспроизвести не успешную покупку, а отмену, ожидание, возврат средств и незавершённую предыдущую транзакцию. Если передать эти ветви внешней платёжной среде, на результат начнут влиять сеть, конфигурация продуктов и состояние тестовой учётной записи. Более надёжный подход — сначала зафиксировать продукты в конфигурационном файле StoreKit на облачном Mac, а затем управлять условиями транзакций через SKTestSession, превратив клиентскую логику в воспроизводимые регрессионные тесты.
Сначала определите границы офлайн-тестирования
Конфигурация StoreKit подходит для проверки сопоставления идентификаторов продуктов, переходов между состояниями покупки, обновления прав доступа, сообщений об ошибках и логики завершения транзакций. Доступ к внешнему платёжному контуру не требуется, а сбои легче воспроизводить.
Однако такие тесты не подтверждают правильность настройки продуктов в рабочей среде и не охватывают серверные уведомления, проверку реальных квитанций и итоговый платёжный интерфейс. Рекомендуется разделить тестирование на три уровня:
| Уровень | Основная цель | Частота запуска |
|---|---|---|
| Модульные тесты | Расчёт прав доступа, сопоставление состояний | При каждом коммите |
| Интеграционные тесты StoreKit | Покупка, отмена, возврат, незавершённые транзакции | При каждом слиянии |
| Приёмка во внешней среде | Конфигурация продуктов, уведомления и обработка квитанций | Перед выпуском |
Ценность офлайн-тестов заключается в сокращении цикла обратной связи, а не в маскировке всех транзакционных рисков одним зелёным флажком.
Зафиксируйте продукты и точку запуска тестов
Создайте в Xcode файл StoreKit Configuration File и оставьте в нём только продукты, необходимые для тестирования. Идентификаторы продуктов должны совпадать с константами в коде. Цены следует использовать только для тестирования интерфейса: бизнес-логика не должна определять права доступа по локализованной строке цены.
Добавьте конфигурационный файл в тестовую Scheme и создайте отдельный план тестирования, например StoreKitRegression.xctestplan. Не используйте Scheme, которую разработчики запускают в повседневной работе: ручное изменение может незаметно повлиять на условия CI.
Тестовая цель может пересоздавать сеанс в setUpWithError():
import StoreKitTest
import XCTest
final class PurchaseRegressionTests: XCTestCase {
private var session: SKTestSession!
override func setUpWithError() throws {
session = try SKTestSession(configurationFileNamed: "Products")
session.disableDialogs = true
session.clearTransactions()
session.failTransactionsEnabled = false
}
override func tearDownWithError() throws {
session.clearTransactions()
session = nil
}
}
Имя конфигурационного файла указывается без расширения. Если инициализация завершается ошибкой, сначала убедитесь, что файл включён в тестовую цель, а затем проверьте, использует ли Scheme ту же конфигурацию.
Разделите ветви транзакций на независимые сценарии
Не проверяйте успешную покупку, возврат и отмену последовательно в одном длинном тесте. Чем больше состояний задействовано, тем сложнее после сбоя определить, на каком шаге возникло загрязнение. Как минимум выделите следующие сценарии:
Обычная покупка и восстановление
Проверьте, что права доступа обновляются сразу после покупки и остаются доступными после повторного создания объектов бизнес-логики. До завершения теста убедитесь, что приложение обработало и завершило транзакцию, иначе следующий сценарий может снова получить старое обновление.
Отмена и внедрение сбоев
При отмене не должно появляться сообщение «Платёж не выполнен», а состояние разблокировки не должно сохраняться. При сетевой или общей ошибке транзакции, напротив, необходимо сохранить возможность повторной попытки. После внедрения ошибки через свойства сеанса восстановите значения по умолчанию до окончания текущего теста, не полагаясь на очистку в следующем сценарии.
Возврат и незавершённые транзакции
После возврата следует отозвать соответствующие права доступа, не удаляя по ошибке другие продукты, которые всё ещё действительны. Для незавершённой транзакции нужно проверить, что приложение продолжает её обработку после перезапуска, а не ограничиваться однократной проверкой обратного вызова кнопки.
Тесты, затрагивающие состояние одного и того же продукта, рекомендуется выполнять последовательно. Если параллельные тесты используют общий симулятор и конфигурацию, они могут удалять транзакции друг друга, вызывая нестабильные сбои, которые проявляются только в CI.
Зафиксируйте условия запуска в командной строке
Сначала просмотрите доступные симуляторы, затем задайте имя устройства, версию системы и путь к Xcode в переменных CI. Не позволяйте xcodebuild автоматически выбирать произвольную цель.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
RESULT_DIR="$PWD/TestResults"
rm -rf "$RESULT_DIR"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-StoreKitTests \
-testPlan StoreKitRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "$RESULT_DIR/StoreKit.xcresult"
Если на OakVM одновременно выполняется несколько конвейеров, выделите каждому заданию отдельный рабочий каталог и симулятор. Не используйте совместно DerivedData, каталог результатов или уже запущенные устройства. Перед началом можно выполнить xcrun simctl shutdown all, но эта команда повлияет на другие задания на том же узле, поэтому подходит только для схемы, при которой один узел полностью выделен одному заданию.
Архивируйте доказательства и задайте критерии отказа
При сбое теста обычно недостаточно сохранить лишь несколько десятков последних строк консольного вывода. Следует архивировать xcresult, хеш коммита, контрольную сумму конфигурационного файла StoreKit, версию Xcode и версию среды выполнения симулятора. Изменения конфигурационного файла также должны проходить проверку кода, чтобы идентификаторы продуктов не удалялись случайно.
Сначала можно записать сводку окружения:
xcodebuild -version
xcrun simctl list runtimes
shasum -a 256 Tests/StoreKit/Products.storekit
Контрольный список должен включать следующие пункты:
- Создаёт и очищает ли каждый тест собственный
SKTestSession - Отключены ли автоматические диалоговые окна, способные заблокировать выполнение без участия оператора
- Проверяются ли отдельно состояние интерфейса и права доступа при отмене, сбое и возврате
- Зависит ли тест от порядка выполнения или предыдущей транзакции
- Загружается ли
xcresultдаже после сбоя - Не содержат ли журналы конфиденциальные учётные данные и полные сведения о транзакциях
После фиксации этих условий регрессионное тестирование встроенных покупок больше не зависит от ручных действий. Внешняя среда отвечает только за те проверки, для которых она действительно необходима, а быстрые, изолированные и отслеживаемые тесты защищают клиентский конечный автомат при повседневных изменениях кода.
Часто задаваемые вопросы
Могут ли офлайн-тесты StoreKit полностью заменить проверку в реальной среде транзакций?
Нет. Они проверяют состояния клиента и ветви ошибок, но реальные настройки товаров, серверные уведомления, обработку квитанций и платёжный интерфейс нужно тестировать отдельно.
Почему тест покупки проходит отдельно, но нестабилен в полном наборе тестов?
Сначала исключите утечку состояния транзакций. Создавайте новую SKTestSession для каждого теста, очищайте операции и последовательно запускайте сценарии, меняющие один товар.
Какие материалы следует сохранить после сбоя теста в CI?
Сохраните xcresult, журнал теста, версию конфигурации StoreKit, хеш коммита, версию Xcode и версию среды симулятора, не включая чувствительные данные.
Запустите следующую сборку на выделенном физическом узле
Выберите OakVM M4 или OakVM M4 Pro и арендуйте облачный Mac без виртуализации на день, неделю, месяц или квартал. Фактическая доступность узла отображается в консоли в режиме реального времени.