Для тех, кто интересуется темой автоматизации на iOS, у меня две новости — хорошая и плохая. Хорошая: в iOS-приложении для платных сервисов используется только одна точка интеграции — in-app purchases (встроенные в приложение покупки). Плохая: Apple не предоставляет никаких инструментов для автоматизации тестирования покупок.

В этой статье я предлагаю вам вместе со мной поискать универсальный метод автоматизации по ту сторону добра и зла Apple. Статья будет полезна всем, кто интегрирует в свои приложения сторонние сервисы, представляющие собой «чёрный ящик»: рекламу, стриминг, управление локацией и др. Обычно такие интеграции очень сложно тестировать, так как отсутствует возможность гибкой настройки стороннего сервиса для тестирования приложения.



Меня зовут Виктор Короневич, я Senior Test Automation Engineer в Badoo. Занимаюсь мобильной автоматизацией более десяти лет. Вместе с моим коллегой Владимиром Солодовым мы выступали с этим докладом на конференции Heisenbug. Он также помог мне в подготовке этого текста. 

В предыдущей статье мы описали, какие методы используются в Badoo для тестированиия интеграций с платёжными провайдерами, которых у нас более 70. В этом материале мы подробнее расскажем о том, как нам удалось добиться стабильной и недорогой автоматизации тестирования платных сервисов в iOS-приложении. 

Давайте начнём с общего описания нашего исследования:

  1. Определение проблемы
  2. Постановка задачи
  3. Решение №1. Песочница Apple
  4. Решение №2. Метод мока функций и использование фейк-объекта
  5. Оценка решения: основные риски
  6. Результат
  7. Заключение

Определение проблемы


Автоматизацию нужно делать тогда, когда в этом возникает естественная потребность. Когда этот момент наступил у нас? 

В приложении Badoo много бесплатных фич, но платные дают пользователю больше возможностей. Получают их двумя способами: за кредиты — внутреннюю валюту Badoo — или купив премиум-подписку. За определённое количество кредитов можно поднять свой профайл в результатах поиска на первое место, сделать подарок другому пользователю и прочее. Премиум-подписка действует определённый период времени и даёт сразу несколько возможностей: включить режим невидимки, видеть людей, проявивших к тебе симпатию, отменить результат своего голосования и другие. 

Эти возможности появлялись в Badoo постепенно. И пару лет назад мы тестировали платные сервисы в iOS-приложениях только вручную. Но по мере появления фич и новых экранов ручное тестирование занимало всё больше времени. Требования об изменениях работы приложения приходили с разных сторон: от разработчиков клиентской части, разработчиков серверной части и даже самого Apple-провайдера. У одного тестировщика одна итерация тестирования начала занимать около восьми часов. Получить быстрый фидбек для разработчика на своей ветке в течение 30 минут стало невозможным, что в конечном итоге могло негативно отразиться на конкурентоспособности продукта. 

Мы захотели получать результаты тестирования как можно быстрее. И столкнулись с проблемой: как недорого организовать регрессионное тестирование платных сервисов в наших iOS-приложениях, чтобы получать быстрые и стабильные результаты? 

Постановка задачи


Итак, с учётом специфики нашего процесса доставки конечного продукта и размера команды мы хотим:

  • тестировать любые покупки внутри клиентского приложения (одноразовые платежи и подписки);

  • повторять итерации тестирования 10–20 раз в день;
  • получать результаты тестирования ~150 тестовых сценариев менее чем за полчаса;
  • избавиться от шумов;
  • иметь возможность прогонять тесты на конкретной ветке кода разработчика независимо от результатов других прогонов.

Теперь, когда мы сформулировали задачу, пора начать путешествие в прекрасный мир инженеров и их решений.

Решение №1. Песочница Apple


В первую очередь мы начали искать информацию об организации автоматического тестирования платных сервисов в документации Apple. И ничего не нашли. Поддержка автоматизации выглядит очень скудной. Если что-то и появляется, то настройка автоматизации предлагаемыми инструментами вызывает сложности (давайте вспомним хотя бы UIAutomation, а также время, когда появилась первая утилита xcrun simctl для iOS Simulator) и приходится искать инженерные решения в том числе в open-source-сегменте. 

В документации Apple для тестирования платных сервисов можно найти лишь Apple Sandbox. Было непонятно, как эту песочницу прикрутить к автоматизации, но мы решили серьёзно исследовать это решение. Уверенности придавало то, что у Android песочница была стабильной и к тому времени мы уже успешно написали тесты на Android. Может быть, и песочница Apple окажется такой же хорошей?

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

1. Пул тестовых пользователей


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

Для запуска всего одного автотеста покупки подписки нам нужно:

  1. взять нового пользователя для авторизации в песочнице;
  2. изменить на симуляторе текущий привязанный Apple ID;
  3. залогиниться в приложении Badoo пользователем Badoo;
  4. дойти до скрина покупки подписки и выбрать продукт;
  5. подтвердить покупку и авторизоваться через Apple ID;
  6. убедиться, что покупка прошла успешно;
  7. отправить на очистку пользователя Badoo;
  8. очистить пользователя песочницы от подписок.

Если попытаться сразу использовать этого же пользователя в следующем тесте, то купить вторую подписку будет невозможно. Нужно подождать, пока первая подписка «протухнет», или отписаться в настройках. Как мы говорили в первой статье, в песочнице есть определённое время действия подписки. Если купить подписку «на месяц», то придётся ждать пять минут для её автоматического закрытия. Сам процесс отписки тоже происходит небыстро.

Соответственно, для нового прогона того же теста нам нужно будет или подождать, пока подписка закончится, или взять другого, «чистого», пользователя. Если мы хотим запустить два теста одновременно независимо друг от друга, то нужно, чтобы в пуле было как минимум два пользователя песочницы. Таким образом, для запуска 100 автотестов параллельно в 100 потоков нам нужно 100 разных пользователей. 

А теперь давайте представим, что мы делаем прогон автотестов на двух агентах, каждый из которых может запускать их в 100 потоков. В этом случае нам нужно уже как минимум 200 пользователей!

2. «Плохие» нотификации


Ну ладно, чем чёрт не шутит! Мы организовали пул пользователей и начали смотреть, как бегают тесты. Они падали по дороге, но большинство — по новым, неизвестным для нас, причинам. Мы начали разбираться и поняли, что при авторизации, подтверждении покупки и работе пользователем в песочнице App Store присылает алерты: например, просит ввести заново имя и пароль, подтвердить авторизацию нажатием на кнопку «ОК», выдаёт информацию о внутренней ошибке с кнопкой «ОК». Иногда они появляются, иногда — нет. И если появляются, то всегда в разном порядке.



Как это возможно, что подозрительная ошибка просто игнорируется в автотесте? А если прилетит реальная ошибка, то что делать? Эта область автоматически становилась для нас «слепой зоной», и нам пришлось писать специальные обработчики для всех возможных алертов, которые могут прилететь от App Store. 

Всё это делало тесты очеееееень медленными:

  • алерты могли прилететь на разных шагах тестового сценария, разрушая основную идею теста — Predictable Test Scenario; нам приходилось дописывать обработчик ошибок, ожидавший появления возможной серии известных игнорируемых алертов;
  • иногда прилетали новые вариации алертов или происходили другие ошибки, поэтому мы вынуждены были перезапускать упавшие тесты; это увеличивало время прогона всех тестов.

3. А был ли тест?


Итак, пользователи в пуле блокируются, потом очищаются в течение n минут. Мы гоняем тесты в 120 потоков, и пользователей в пуле уже довольно много, но этого недостаточно. Мы сделали нашу систему менеджмента пользователей, сделали обработчик алертов — и тут случилось ЭТО. Песочница стала недоступной на пару дней для любого тестового пользователя. 

Этого никто не ожидал. И это была последняя капля в чаше нашего терпения, которая окончательно убила любовь к песочнице Apple и заставила нас встать на путь по ту сторону добра и зла. Мы поняли, что нам не нужна такая автоматизация и что мы не хотим больше мучаться с этим опасным решением.

Решение №2. Метод мока функций и использование фейк-объекта


Итак, мы хлебнули проблем с автоматизацией в песочнице Apple. Но не думайте, что в мобильном мире всё совсем плохо. На Android песочница намного стабильнее — там можно гонять автотесты. 

Давайте попробуем найти другое решение для iOS. Но как искать? Куда смотреть? Заглянем в историю тестирования и разработки ПО: что было до безумного мира Apple? что говорят люди, которые написали кучу книжек и заслужили авторитет в мире автоматизации и разработки ПО? 

Я сразу вспомнил про труд «xUnit Test Patterns: Refactoring Test Code», написанный Джерардом Месарошем (рецензия Мартина Фаулера), — на мой взгляд, одну из лучших книг для любого тестировщика, который знает хотя бы один язык программирования высокого уровня и хочет заниматься автоматизацией. Пара глав этой книги, посвящённые тестированию SUT в изоляции от других компонентов приложения, которые являются для нас «чёрным ящиком», смогут нам помочь. 

1. Введение в моки и фейки


Нужно отметить, что в мире автоматического тестирования не существует общепринятой границы между понятиями Test Doubles, Test Stub, Test Spy, Mock Object, Fake Object, Dummy Object. Всегда нужно учитывать терминологию автора. Нам нужны всего два понятия из большого мира Test Doubles: мок функции и фейк-объект. Что это? И зачем нам это нужно? Дадим краткое определение этих понятий, чтобы у нас не возникало разногласий.

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

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

А подмену сущности, полученную в результате выполнения функции, на сущность поддельную (содержащую нужные данные в полях, а иногда даже испорченные данные) будем называть внедрением фейк-объекта. Более подробно об этом можно прочитать в книге, которую я упомянул выше, или в любом другом компендиуме по тестированию и разработке ПО. 

Чтобы закончить с этим, давайте подчеркнём некоторые особенности использования мока функций и фейк-объектов:

  1. Для того чтобы мокать функции, нужно обращаться к исходному коду и знать, как работает приложение с компонентом изнутри на уровне разработчика.
  2. Для того чтобы внедрить фейк-объект, нужно знать структуру настоящего объекта.
  3. Использование мока функции даёт возможность гибкой настройки работы приложения с компонентом.
  4. Использование фейк-объекта позволяет наделять сущность любыми свойствами.

Метод моков и фейк-объекта идеально подходит для изолирования работы компонента внутри приложения. Давайте посмотрим, как мы можем применить этот метод для решения нашей задачи, где в качестве компонента будет App Store. В силу особенностей использования данного метода сначала нам нужно обратиться к изучению природы работы нашего приложения с компонентом, а затем — к технической реализации, чтобы сделать конкретные моки и фейк-объект. 

2. Как происходит реальная покупка


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

  • пользователь приложения — любой актор, который совершает действия с приложением, им может быть человек, а может быть скрипт, который выполняет необходимые инструкции;
  • приложение (в нашем случае мы используем iOS-приложение Badoo, инсталлируемое в iOS-симулятор);
  • сервер — актор, который обрабатывает запросы от приложения и отправляет обратно ответы или асинхронные уведомления без запроса клиента (в данном случае мы имеем в виду один абстрактный сервер Badoo, чтобы упростить структуру);
  • App Store — актор, который является для нас «чёрным ящиком»: мы не знаем, как он устроен внутри, но знаем его публичный интерфейс для обработки покупок внутри приложения (StoreKit framework), а также умеет проверять данные на сервере Apple.

Давайте посмотрим, как происходит покупка. Весь процесс можно увидеть на схеме: 


Рисунок 1. Cхема платежа в App Store

Распишем пошагово основные действия акторов.

1. Начальная точка — состояние всех акторов до открытия экрана со списком продуктов.

Что это за экран и как мы на него попали? 

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

Если пользователь выбрал подарок и у него нет денег на счёте, то он увидит список разных пакетов кредитов (Payments Wizard) для покупки. Начальная точка в нашем примере — это список подарков. На схеме мы можем считать такой точкой любой экран до показа списка продуктов для покупки кредитов или подписки.

2. Открытие списка продуктов. 

Мы находимся в начальной точке, например на списке подарков. Пользователь выбирает один из подарков в приложении. Приложение делает запрос на наш сервер, чтобы получить список возможных Product ID пакетов кредитов (100, 550, 2000, 5000). Сервер возвращает этот список приложению. 

Далее приложение отправляет полученный список Product ID на проверку актору App Store (системный iOS-фреймворк StoreKit, который ходит на сервер Apple). Он возвращает список проверенных продуктов — и в итоге приложение показывает пользователю финальный список пакетов кредитов с иконками и ценами.

3. Выбор продукта и генерация чека.

Пользователь выбирает платный продукт. App Store требует подтверждение покупки и авторизацию через Apple ID. После успешной авторизации пользователя управление передаётся приложению. Приложение ожидает генерации чека (receipt) внутри собственного пакета. Пользователь в это время видит солнышко, которое блокирует экран. То, что receipt сгенерировался, можно понять, используя метод appStoreReceiptURL класса Bundle. После того как чек сгенерирован App Store, приложение подбирает чек из своего пакета и отправляет запрос с чеком и пользовательскими данными на сервер Badoo.

4. Проверка чека на сервере Badoo.

Как только сервер Badoo получает данные о чеке и о пользователе, он отправляет их обратно на сторону сервера Apple, чтобы осуществить первый цикл проверки. Это одна из рекомендаций Apple. Затем на этом первом цикле проверки сервер получает информацию о текущем состоянии подписки.

5. Отправка пуш-уведомления (push notification) c сервера.

Сервер Badoo снова обрабатывает полученную информацию после проверки со стороны Apple и отправляет приложению ответ вместе с пуш-уведомлением.

6. Пуш-уведомление в приложении.

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

3. Определение зависимостей и контура тестирования



Для дальнейшего разговора введём ещё два понятия — внешняя зависимость и контур тестирования.

Внешняя зависимость


Под внешними зависимостями мы будем понимать любое взаимодействие с компонентом, который является для нас «чёрным ящиком». В данном случае в качестве такого компонента выступает App Store в виде системного iOS-фреймворка (StoreKit), с которым работает наше iOS-приложение, и сервера Apple, куда уходят запросы на проверку. 

Управление этими зависимостями в реальных условиях невозможно, приложение вынуждено реагировать на выходные сигналы от «чёрного ящика» (см. рис. 2). 

У нас три внешних зависимости:

  1. Проверка продуктов StoreKit.
  2. Получение и подмена чека покупки.
  3. Проверка чека на сервере Badoo.


Рисунок 2. Внешние зависимости 

Контур тестирования


Контур тестирования — это участки пути, которые мы будем проходить и проверять в процессе тестирования. 


Рисунок 3. Контур тестирования 

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

Рассмотрим последовательно каждую зависимость.

4. Изолирование зависимостей: техническая реализация


У нас в компании для реализации платежей была взята PPP-концепция, в основе которой лежит интерфейс Payment Provider. Это основной интерфейс взаимодействия с актором App Store (StoreKit) внутри нашего приложения, у которого есть два основных метода:

  1. prepare — метод, который отвечает за проверку продуктов;
  2. makePayment — метод, который обрабатывает покупку в приложении.

Все платежи на iOS были отрефакторены в соответствии с этой концепцией, что позволило получить простой и удобный класс Mock Payment Provider. Это основной интерфейс взаимодействия с удобной копией поведения StoreKit внутри нашего приложения. Что означает «удобной копией»? У данного провайдера есть моки методов prepare и makePayment, которые делают то, что мы хотим. Давайте рассмотрим на примере кусочков кода, как нам удалось интегрировать моки.

Зависимость №1. Проверка продуктов StoreKit


Для проверки списка продуктов используется функция prepare, которая возвращает список проверенных продуктов. Мы можем использовать мок, в котором отключим проверку и вернём входящий список продуктов как полностью проверенный. Таким образом, зависимость будет устранена. 


Рисунок 4. Схема устранения первой зависимости

На самой вершине архитектуры в нашем приложении находится Payment Provider. Он отражает интерфейс возможного провайдера в приложении. Код реализации моков можно найти в классе Mock Payment Provider. 

public class MockPaymentProvider: PaymentProvider {
   public static var receipt: String?
   public static var storeKitTransactionID: String?

   public func prepare(products: [BMProduct]) -> [BMProduct] {
      return products
   }
   ...
}

Листинг 1. Мок клиентской проверки 

У Mock Payment Provider мы можем увидеть реализацию метода prepare. Магия мока оказывается очень простой: в методе пропущена проверка продуктов на стороне StoreKit, и он просто возвращает входящий список продуктов. Реальная реализация prepare выглядит так:

public func prepare(products: [BMProduct]) -> [BMProduct] {
   let validatedProducts = self.productsSource.validate(products: products)
   return validatedProducts
}

Листинг 2. Реальный Store Payment Provider

Зависимость №2. Получение и подмена чека покупки


Со второй зависимостью дело обстоит немного сложнее: нам нужно сначала убрать авторизацию, чтобы не держать пул аккаунтов пользователей, а потом каким-то образом получить сам чек. Форму авторизации мы можем просто удалить:


Рисунок 5. Удаление формы авторизации при проведении платежа

С чеком всё не так просто. Появляется много вопросов:

  1. Как заранее получить чек для нужного продукта?
  2. Если мы всё же получили чек, то когда и как его подложить внутри приложения?

Здесь у актора «Пользователь» появляется новая роль — QA. Когда мы прогоняем тест, мы можем не только кликать на кнопки интерфейса, но также вызывать методы API тестового фреймворка (методы, которые симулируют действия пользователя) и REST API-сервисов (методы, которые могут творить магию со стороны внутреннего сервиса Badoo). У нас в Badoo используется очень мощный инструмент QA API (со всеми его возможностями вы можете ознакомиться по ссылке: https://vimeo.com/116931200). Именно он помогает нам в тестировании и даёт чек для нужного продукта на стороне сервера Badoo. Сервер Badoo — это лучшее место для генерации чеков: там есть шифровка и дешифровка чека, поэтому сервер знает всё об этой структуре данных. 

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


Рисунок 6. Схема получения чека 

Как это стало возможно технически? 

1. Для выставления фейкового чека в приложении мы смогли использовать бэкдор, который сохранил фейковый чек в поле receipt MockPaymentProvider:

#if BUILD_FOR_AUTOMATION

@objc
extension BadooAppDelegate {
    
   @objc
   func setMockPurchaseReceipt(_ receipt: String?) {
      PaymentProvidersFactory.useMockPaymentProviderForITunesPayments = true
      MockPaymentProvider.receipt = receipt
   }
   ...
}

#endif

Листинг 3. Бэкдор выставления фейкового чека 
 
2. Приложение смогло взять наш чек благодаря MockPaymentProvider, в котором мы использовали мок makePayment и сохранённый чек в MockPaymentProvider.receipt:

public class MockPaymentProvider: PaymentProvider {
   ...
   public func makePayment(_ transaction: BPDPaymentTransactionContext) {
      ...
      if let receiptData = MockPaymentProvider.receipt?.data(using: .utf8) {
         let request = BPDPurchaseReceiptRequest(...)
         self.networkService.send(request, completion: { [weak self] (_) in
            guard let sSelf = self else { return }
            if let receipt = request.responsePayload() {
               sSelf.delegate?.paymentProvider(sSelf, didReceiveReceipt: receipt)
            }
         })
      } else {
         self.delegate?.paymentProvider(self, didFailTransaction: transaction)
      }
   }
}

Листинг 4. Вызов мока обработки покупки с фейковым чеком

3. Получение фейкового чека

Для получения фейкового чека мы использовали метод на сервере (см. листинг 5). Он берёт дефолтный массив с данными для генерации данных чека и добавляет в него данные, которые нужны для конкретного продукта.

$new_receipt_model = array_replace_recursive(
   //создаём массив со схемой чека по умолчанию
   $this->getDefaultModel(),
     
   //определяем дополнительные параметры на основе сохранённых данных
   //необходимо, если обрабатываем ранее использованный платёж
   $this->enrichModelUsingSubscription($nr),
     
   //определяем дополнительные параметры в зависимости от желаемого состояния
   $this->enrichModelUsingInput($input)
);

//создаём подпись
$new_receipt = $this->signReceipt(
   json_encode($new_receipt_model, true),
   $new_receipt_model
);

Листинг 5. Серверная часть генерации чека

Чтобы повторить структуру реального чека, кастомный чек, отправляемый приложением, должен быть зашифрован с применением сертификата. Мы используем наш рабочий сертификат вместо сертификата Apple.

function signReceipt($receipt, $response) ?{
   //добавляем заголовки и кодируем чек base64?
   $receipt = 'Subject: ' . base64_encode(json_encode($response)) . PHP_EOL .  PHP_EOL . $receipt;?
   file_put_contents($receipt_file, $receipt);
   ...
   
   //подписываем чек нашим сертификатом
   $sign_result = openssl_pkcs7_sign(?$receipt_file, $signed_receipt_file, 'file://'.$path_cert, 'file://'.$path_key, [], PKCS7_BINARY);?
   ...?
   
   //добавляем заголовки?
   $signed_content_with_headers = file_get_contents($signed_receipt_file);?
   list($headers, $signed_content) = explode(PHP_EOL . PHP_EOL, $signed_content_with_headers);?    
   
   //возвращаем чек    
   return str_replace(["\r\n", "\r", "\n"], '', $signed_content);
?}

Листинг 6. Метод для подписи чека сертификатом

4.  В итоге в тесте мы получаем:

И(/Я генерирую новый платёжный чек для покупки "((\d+) кредитов|подписки на (\d+) месяца?/) do |service_type|
  # обработка типа сервиса
  service_details = parse_options(service_type)

  # вызов QA API (внутренний сервис Badoo)
  receipt = QaApi::Billing.order_get_app_store_receipt(service_details)

  # вызов бэкдора
  Backdoors.set_fake_receipt(receipt)
end

Листинг 7. Шаг теста на языке Gherkin для фреймворка Cucumber 

Зависимость №3. Проверка чека на сервере Badoo


Чтобы убрать третью зависимость, нужно избавиться от верификации чека на сервере. Здесь важно помнить о том, что верификация делается в два этапа. На первом этапе выполняется проверка подлинности чека на основе подписей и сертификатов. На втором — чек отправляется в App Store. В случае успешной валидации на этом этапе мы получим расшифрованный чек, который можно обработать.  


Рисунок 7. Удаление серверной верификации

Сначала сервер выполняет первичную верификацию чека в методе  verifyReceiptByCert родительского класса. Здесь проверяется подпись сертификатом App Store. В случае с фейковым чеком эта верификация будет неудачной, потому что он подписан нашим сертификатом, и мы вызовем метод для верификации локальным сертификатом verifyReceiptByLocalCert. В этом методе мы попробуем расшифровать чек, используя локальный сертификат, и в случае успеха результат расшифровки поместим во внутреннее поле local_receipt дочернего класса (метод addLocallyVerifiedReceipt).


class EngineTest extends Engine 
   function verifyReceiptByCert($receipt) ?{
      $result = parent::verifyReceiptByCert($receipt);?
      if ($result === -1 || empty($result)) {
         $result = $this->verifyReceiptByLocalCert($receipt);?
      }?
      return $result;
?   }

   function verifyReceiptByLocalCert($receipt) {
      $receipt_file = tempnam(sys_get_temp_dir(), 'rcp');
      file_put_contents($receipt_file, base64_decode($receipt));
      $result = openssl_pkcs7_verify($receipt_file, PKCS7_BINARY, '/dev/null', [$DIR]);
      if ($result) {
         $this->addLocallyVerifiedReceipt($receipt, base64_decode($response));
      }
      unlink($receipt_file);
      return $result;
   }

class Engine 
   function verifyReceiptByCert($receipt) {
      $receipt_file = tempnam(sys_get_temp_dir(), 'rcp');
      file_put_contents($receipt_file, base64_decode($receipt));
      $result = openssl_pkcs7_verify($receipt_file, PKCS7_BINARY, '/dev/null', [$DIR]);
      unlink($receipt_file);
      return $result;
   }

Листинг 8. Первичная верификация

Во время вторичной верификации (verifyReceipt) мы получаем значение поля local_receipt дочернего класса getLocallyVerifiedReceipt. Если оно не пустое, то мы используем его значение как результат верификации. 

Если же поле пустое, то мы вызываем вторичную верификацию из родительского класса (parent::verifyReceipt). Там мы делаем запрос к App Store для верификации на его стороне. Результатом верификации в обоих случаях является расшифрованный чек.

class EngineTest extends Engine
   function verifyReceipt($receipt_encoded, $shared_secret, $env) {
      $response = $this->getLocallyVerifiedReceipt($receipt_encoded);
      if (!empty($response)) {
          return json_decode($response, true);
      }
      return parent::verifyReceipt($receipt_encoded, $shared_secret, $env);
   }

class Engine
   function verifyReceipt($receipt_encoded, $shared_secret, $env) {
      $response = $this->_sendRequest($receipt_encoded, $shared_secret, $env);
      return $response;
   }

Листинг 9. Вторичная верификация

5. Видео прогона тестов: покупка кредитов и подписки


Тест №1. Покупка подписки


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

Видео прогона теста:


Тест №2. Покупка кредитов и отправка подарка


Когда
Я логинюсь в приложении новым пользователем с фото
И
Я добавляю десять кредитов в свой профиль
И
Я генерирую новый платёжный чек на 550 кредитов
И
Я создаю нового пользователя Leela
И
Leela проголосовала «Да» за меня
И
Я захожу в People Nearby и открываю профиль Leela
И
Я голосую «Да» за Leela
То
Я проверяю матч-страницу
Когда
Я выбираю отправить обычный подарок
То
Я проверяю платёжный экран со списком пакетов 
Когда
Я выбираю купить 550 кредитов
То
Я проверяю нотификацию об успешной покупке
И
Я убеждаюсь, что Leela получила подарок в чате


Видео прогона теста:



Оценка решения: основные риски


Удаление внешних зависимостей сопряжено с определёнными рисками.
 
1. Неправильная конфигурация.

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

2. Пограничные случаи.

Например, когда платёж полностью проходит, пользователь получает уведомление о том, что он совершён, но наше приложение не может найти чек, который должен быть подложен в результате выполнения этого платежа. Риск заключается в том, что мы сами подкладываем чек с помощью бэкдора, и такой кейс мы, естественно, отследить не можем. Чтобы как-то компенсировать этот риск, мы проводим end-to-end проверки с помощью песочницы или реального платежа после релиза.
 
3. Недобросовестная подделка или фрод.

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

4. Изменение формата чека.

Это самый серьёзный риск. Изменение формата чека возможно, когда Apple что-то меняет, не предупредив нас. У нас такой кейс был: при переходе на iOS 11 полностью изменился формат чека. Мы генерировали фейковый чек на своём сервере и использовали его в тесте. У нас всё было прекрасно: все поля на месте, всё замечательно, всё обрабатывается. Но когда мы переходили в реальную систему, ничего не работало. Поля, которые в чеке были значимыми, просто переставали существовать. 

Как компенсировать этот риск? Во-первых, мы не исключаем проведение end-to-end-тестирования песочницы до релиза и реальным платежом — после релиза. Сейчас у нас в активной фазе находится проект по проверке нотификаций, когда мы все чеки, которые получаем с продакшена, пытаемся классифицировать по принципу, понимаем мы, что это такое, или не понимаем. Если ответ отрицательный, то мы начинаем обрабатывать всё вручную, смотреть, что изменилось, что не так, что нужно поменять в своей системе. 
Риск
Причина
Как компенсировать
неправильная конфигурация
удаление проверки
юнит-тест на сервере
эдж-кейсы
(чек не доставлен)
использование бэкдора
E2E-проверки (песочница и реальный платёж)
недобросовестная подделка, фрод
генерация нотификации и чека на сервере
собственный сертификат
изменение формата чека
генерация нотификации и чека на сервере
проверка реальных нотификаций и чека на проде (новый проект),
E2E-проверки (песочница и реальный платёж)

Результат



Рассмотрим основные преимущества, которые мы смогли получить в результате применения метода моков и фейк-объекта.

Недорогая, быстрая и стабильная автоматизация платных сервисов на iOS


Вместе с командой ручного тестирования на iOS (особая благодарность Колину Чану) мы смогли написать более 150 автотестов для платежей. Это довольно большой объём покрытия для одной области приложения. 

Благодаря параллелизации мы можем получать результат всего за 15–20 минут на любой ветке разработчика iOS-клиента или разработчика сервера биллинга. До автоматизации тестирование этой области вручную силами одного человека занимало восемь часов. 

Также мы можем тестировать подавляющее большинство тест-кейcов благодаря настройке Mock Payment Provider через моки таким образом, как нам нужно. При помощи моков мы научились как отключать проверку продуктов, так и имитировать кейсы, когда проверка выполняется частично. Таким образом, нам открылись кейсы, которые раньше мы не могли тестировать в принципе.

Функциональная регрессия при разработке новых фич


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

Функциональная регрессия при рефакторинге платежей


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

Тестирование экспериментальных фич от Apple: грейс-период


Подобная система полностью заменима, когда вы тестируете новые интеграции, которые ещё не реализованы в песочнице. Так у нас было с грейс-периодом. В песочнице этой функциональности нет. Грейс-период на Apple пока недоступен для всех. Это экспериментальный проект, который Badoo реализует совместно с Apple. Для того чтобы сделать чек с грейс-периодом, нам необходимо было добавить в него вот такой кусок JSON-кода:

pending_renewal_info:[
{
      expiration_intent: 2
      grace_period_expires_date: 2019-04-25 15:50:57 Etc/GMT
      auto_renew_product_id: badoo.productId
      original_transaction_id: 560000361869085
      is_in_billing_retry_period: 1
      grace_period_expires_date_pst: 2019-04-25 08:50:57 America/Los_Angeles
      product_id: badoo.productId
      grace_period_expires_date_ms: 1556207457000
      auto_renew_status: 1
}]

Листинг 10. Грейс-период для подпиския

Мы это очень легко сделали буквально за несколько секунд. В нашей системе мы смогли протестировать нашу реакцию на новую фичу. Сейчас обкатываем эту функциональность на проде. 

Тестирование качества продукта в композиции методов


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

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

Заключение


В результате нашего исследования мы получили недорогой, быстрый и стабильный метод тестирования не только платных сервисов на iOS, но и любых компонентов, встраиваемых в приложение как «чёрный ящик». Сейчас мы в Badoo внедряем данный метод для тестирования на Android платных провайдеров (Global Charge, Boku, Centili), которые имеют нестабильные песочницы или любые другие ограничения. Также мы используем метод моков для тестирования рекламы, стриминга и геолокации. 

Стоит сказать, что сам процесс внедрения нового метода не был быстрым. Приходилось договариваться с четырьмя командами: iOS QA, iOS Dev, Billing QA, Billing Dev. Не все хотели переходить на новый метод, опасаясь рисков. Иногда это было и догматическое следование: мы много лет тестировали в песочнице, и основной силой, которая смогла разрушить догму, стало желание тестировщиков биллинга и iOS-платформы изменить ситуацию и избавиться от мучений. Позже разработчики осознали такие преимущества данного метода, как точная диагностика (мы смогли находить не баги песочницы, а баги нашего клиента или сервера), гибкость в настройке компонента (мы смогли легко тестировать негативные кейсы на интеграционном уровне) и, конечно же, ответ в течение 30 минут на ветке с разрабатываемым кодом.

Всем, кто дочитал до конца, огромное спасибо. Всем, кто помогал и участвовал в данном проекте, также огромное спасибо. Отдельная благодарность летит этим людям: 

  • Пётр Колпащиков — iOS-разработчик, который помог сделать моки на стороне клиента и разработал PPP-концепцию;
  • Владимир Солодов — Billing QA, который помог с QA API для генерации фейк-чеков и моком проверки со стороны биллинг-сервера;
  • Максим Филатов и Василий Степанов — Billing Dev Team, которые помогли с кодом биллинг-сервера;
  • iOS Dev Team — разработчики, которые смогли отрефакторить наши платежи в новой концепции, благодаря чему использование моков стало возможным;
  • iOS QA Team — потрясающая команда тестирования, которая написала кучу автотестов;
  • Billing QA Team — тестировщики, которые помогали исследовать проблемы.

Комментарии (0)