Хабрахабр

Автоматизация тестирования платных сервисов на iOS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

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

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

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

Сервер 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 ...
}

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

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

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

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

#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.

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


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.

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

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. Покупка кредитов и отправка подарка

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

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

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

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

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

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

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

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

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

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

Результат

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

Недорогая, быстрая и стабильная автоматизация платных сервисов на 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 — тестировщики, которые помогали исследовать проблемы.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть