Тестирование приложений через сквозные (end-to-end) тесты сейчас довольно популярно. Этот вид тестирования позволяет оценить работоспособность приложения со стороны пользователя. Поэтому компания, в которой я работаю, внедряет этот вид тестирования в проекты. Летом 2022 года мы разрабатывали блокчейн приложение. Моя задача заключалась в проведении E2E тестирования DApp . Но мы столкнулись с проблемой — готовых решений для проведения end-to-end тестирования DApp под нашу конкретную задачу не было. Эта статья о том, как мы решали эту проблему. Всем привет! Меня зовут Илья. Я QA-engineer в компании Tourmaline Core. Я занимаюсь налаживанием процессов тестирования на всех уровнях. В этой статье хочу рассказать с какими проблемами я столкнулся на одном из проектов, как решал их и какой результат получил. Задачей проекта, над которым мы работали, была разработка Telegram-бота для взаимодействия с Gnosis Safe. Gnosis Safe  является безопасным способом управления криптовалютными средствами. Также, Gnosis Safe — это система, позволяющая создавать DAO (децентрализованная автономная организация). Сервис позволяет ее участникам управлять организацией с помощью proposal (предложений), где выполнение каждого пропозала определяется коллективным голосованием, без центрального органа управления. Так, любой пользователь внутри данной системы может создать пропозал на добавление нового участника DAO или выполнения метода смарт-контракта, за который голосуют другие участники DAO. Когда количество проголосовавших переходит некий порог, то в блокчейне создается и выполняется транзакция. Бот состоит из Web App, а для обработки полученных команд разработан API и настроена интеграция с мобильным приложением Metamask при помощи WalletConnect. Приложение Metamask используется для подтверждения или отклонения транзакции, а WalletConnect обеспечивает подключение нашего приложения к криптокошельку. Для начала немного расскажу, как происходит взаимодействие пользователя с DAO в Gnosis Safe с помощью бота. Создаем группу в Telegram и добавляем туда бота Выполняем все шаги для инициализации DAO Открываем UI в боте С помощью приложения Metamask сканируем сгенерированный QR-код В приложении Metamask подтверждаем запрос на подключение Выбираем нужное действие в UI бота (например, перевод эфира со счета нашего DAO на другой счет) Открываем приложение Metamask и во всплывающем окне подтверждаем транзакцию В результате был разработан бот с собственным графическим интерфейсом и API для него. API получает запросы от UI на создание и выполнение пропозалов в Gnosis Safe, который в свою очередь и взаимодействует с DAO, который был в нем создан. На рисунке изображена схема взаимодействия сервисов в проекте. Для меня (как и для многих, пожалуй) это дебри, в которые погружаться страшно, но интересно. От меня требовалось создать автоматизированные тесты для проверки подключения нашего приложения к Metamask по протоколу WalletConnect и проверки возможности создания транзакций после этого. То есть нужно пройти все этапы взаимодействия пользователя с нашим приложением — от сканирования QR-кода, до успешного коннекта, после которого можно отправлять запросы и видеть уведомления. Но с приложениями на блокчейне есть проблема — их мало кто тестирует через сквозные тесты, так как для этого имеется мало подходящих опциональных инструментов. В нашем случае проблемой стало использование мобильного приложения Metamask, вместо браузерного расширения. Я начал искать решения и здесь мое гугление, которое обычно не заходит дальше первой страницы, дошло до пятой, но результата так и не дало. Я не нашёл ни статей, ни наглядных примеров, ни стоящих инструментов под свою задачу. Всё что было, это вопросы от таких же тестировщиков. Я выделил 2 основных фактора, которые привели меня к разработке своего решения: API Metamask не предоставляет возможность установить соединение с помощью WalletConnect через мобильное приложение. Поэтому иным способом, кроме как сканировать QR-код это не сделать. Это означало, что без работы с пользовательским интерфейсом не обойтись. отсутствие E2E-фреймворков под нашу задачу. Есть Synpress и Dappeteer (по сути, расширения для популярных Сypress и Puppeteer), но они предназначены для тестирования взаимодействия DApp с MetaMask в качестве браузерного расширения. А у нас, напомню, работа идет только с мобильным приложением. Поэтому было принято решение использовать эмулятор смартфона для установки на него приложения Metamask и взаимодействовать с ним, используя библиотеку Detox.   Начало настройки проекта: Мы клонировали себе репозиторий metamask-mobile. Сперва хотелось всё запустить у себя на машине и посмотреть, как работают сквозные тесты, которые уже были написаны разработчиками на фреймворке Detox. Для этого в репозитории есть инструкция, которую нужно проанализировать, чтобы понять подходим ли мы по системным требованиями и что необходимо установить. Сразу видим, что приложение можно запустить только на MacOS или Linux.  Теперь можно начать установку и настройку проекта. Сначала нужно выбрать на какой мобильной операционной системе оно будет запускаться. Мы выбрали Android, т.к. имели опыт работы с ним. Ставим Android Studio IDE, и согласно инструкции создаем эмулятор Google Pixel 3. В ридми не указано какую версию API нужно использовать. У нас все заработало на версии 29. На более свежих версиях Detox не мог взаимодействовать с элементами интерфейса в приложении. После этого нужно выполнить все yarn команды из ридми. Далее можно запускать Metamask в эмуляторе как обычное react-native приложение — с помощью команды yarn start:android. Но выполнив команду для запуска Detox тестов yarn test:e2e:android мы получили ошибки. Чтобы всё правильно заработало, пришлось внести некоторые изменения в пакеты и скрипты запуска. Наконец-то можно было начать переделывать тесты под себя: удалить все тестовые кейсы и оставить один, связанный с авторизацией в кошелек. Для полного покрытия нужного нам сценария осталось добавить: Нажатие на кнопку сканирования QR-кода Ожидание сканирования QR-кода и появления уведомления о подключении Нажатие на кнопку одобрения подключения к нашему DApp Запуск API-тестов Ожидание появления уведомлений о транзакциях, созданных API запросами Нажатие на кнопку подтверждения этих транзакций Для реализации некоторых пунктов понадобилось сделать отдельный проект. Для сканирования QR-кода пришлось делать скриншот при помощи Cypress, а на API написать тесты. Но обо всем по порядку. Сканирование QR кода:   QR-код сканируется с помощью специального сканера внутри приложения Metamask. Этот сканер использует камеру смартфона для считывания. Но как это будет выглядеть в эмуляторе? Разработчики Android Studio сделали виртуальную сцену, как бы эмуляцию реального взаимодействия с внешним миром. Выглядит это будто в игре: вы находитесь в некой квартире, можете по ней ходить, вертеть камерой. В настройках эмулятора можно добавлять свои изображения на специальные темплейты, находящиеся в квартире, например, на доске. Это может пригодиться для сканирования QR-изображений. Я задался вопросом — как в автоматическом режиме без моего участия отсканировать изображение QR-кода, которое я заранее поместил на одном из темплейтов? И наткнулся на очень простое решение. В папке Library / Android / sdk / emulator / resources есть файл под названием Toren1BD.posters. В нем хранится информация о местоположении темплейтов в пространстве, на место которых можно вставлять свои изображения. Этот файл нужно открыть и добавить туда следующие строчки:  Добавить изображение с названием custom.png в эту директорию. Этот png файл должен быть изображением QR-кода. Теперь при открытии камеры у нас появляется изображение, которое мы хотим отсканировать: Но как мы можем автоматически получать скриншот QR-кода с нашего приложения? Здесь к нам на помощь приходит Сypress. Можно создать один небольшой тест, который сделает снимок нужного нам компонента с изображением QR-кода:   Скриншоты сохраняются в папке /screenshots, откуда их в дальнейшем будем доставать при помощи скриптов, переименовывать и перемещать в папку с плейсхолдерами. Код для сканирования QR-кода можно найти в репозитории. Также, сразу после скриншота, нам бы хотелось перетащить получившееся изображение в директорию /resources. Для этого, в Cypress есть плагины, изменяющие или расширяющие его внутреннее поведение (например, эвент after:screenshot). Добавление плагинов происходит через функцию setupNodeEvents(on, config). Туда мы добавляем слушателей, которые триггерятся на заданные события и выполняют инструкции, описанные внутри. Прежде всего, напишем скрипт, который переносит скриншот в нужную нам директорию: Потом добавим функцию setupNodeEvents в файл конфигурации Cypress cypress.config.js, а в неё добавим эвент, запускающий скрипт после скриншота. Если у вас версия Cypress ниже 10.0, то добавление плагинов происходит в файле index.ts директории plugins Хорошо, допустим мы отсканировали код и установили соединение. А что дальше? Следующим шагом нужно получить данные о сессии, которые получает DApp. В дальнейшем, сессия будет нужна API-сервису для отправки запросов. Получение сессии: Как было сказано ранее, WalletConnect устанавливает связь между двумя узлами — DApp и криптокошельком пользователя (в нашем случае Metamask). Когда пользователь нажимает на кнопку “Connect” в приложении, отображается QR-код, который нужно сканировать приложением Metamask. Так происходит подключение кошелька к приложению. Metamask сканирует QR-код, расшифровывает данные запроса на соединение и показывает данные этого запроса пользователю. Далее пользователь может отклонить или одобрить установление соединения. В случае успешного подключения, создается сессия. Благодаря сессии, Metamask сможет получать уведомления с предложением подписать или отклонить транзакцию, созданную с помощью DApp. В нашем случае требуется сохранять сессию в базе данных через вызов API метода. Это необходимо для того, чтобы при подписании транзакции отобразить запрос в приложении Metamask. Для этого сохраненная сессия используется в  провайдере, который предоставляет WalletConnect. Таким образом, при каждом новом запуске Metamask в эмуляторе для автоматизации отправки запросов нам нужно устанавливать соединение между кошельком и сервисом. То есть каждый запуск нужно сохранять новую сессию. В репозитории мы добавили простейшую реализацию UI (dao-service-stub) для отображения QR-кода. В коде можно найти обработчик события onConnect. В нем мы будем получать сессию после того как в Metamask подтверждено подключение. Далее, нам нужно передать сессию в API. Для этого вызываем эндпоинт сохранения сессии: Здесь TelegramData представляет из себя объект: При этом стоить помнить, что API должен быть запущен. Его мы тоже запускаем с помощью скрипта перед запуском Cypress для сканирования кода. После удачного подключения сессия сохраняется в базе данных. API-тесты: Чтобы была возможность проверить успешность подключения по WalletConnect и работоспособность запросов, важно запустить E2E API-тесты. Их цель пройти базовый флоу от создания и инициализации DAO, до создания транзакций, взаимодействующих с Gnosis Safe. Все запросы должны сопровождаться получением уведомлений с предложением подписать или отклонить транзакцию в приложении Metamask. После подтверждения транзакция идет в блокчейн, а ее ответ приходит в наш API-сервис, за счет того же WalletConnect. Для написания тестов на API мы выбрали Pactum JS, так как это достаточно простой, но мощный инструмент. Раннером тестов выбрали Mocha. Пример теста на запрос, инициализирующий DAO: По умолчанию, если ответ не был получен за 3 секунды, то любой запрос падает. Это нужно учитывать, ведь тесту, создающему транзакцию, попросту не хватит этого времени. Так что необходимо изменить значение тайм-аута до 50 секунд — именно столько времени в среднем хватает на получение ответа.Вот пример кода, который увеличивает тайм-аут запроса:  Добавить модуль request:   Затем добавить следующую строчку в любом месте перед тестами:  После отправки в тестах запроса (например, для перевода токенов) мы увидим в эмуляторе всплывающее окно с просьбой подписать сообщение: Теперь любой тест будет ожидать ответ в течение 50 секунд. В конечном итоге мы получили сквозные API тесты, которые имитируют базовый сценарий пользователя: создание группы, инициализация DAO, создание пропозала, голосование за пропозал и др. Код тестов можно посмотреть в репозитории. Теперь все блокеры можно считать разрешенными и можно переходить к нашей основной цели — написанию E2E теста, в котором всё свяжется воедино. Детокс-тесты: Остается переделать Detox тесты под задачу. Само приложение написано на React Native, в котором используется компонентная структура. Поэтому нужно найти необходимые нам компоненты: кнопка открытия камеры для сканирования QR кода модальное окно подтверждения подключения и кнопка его одобрения модальное окно подтверждения транзакции и кнопка его одобрения модальное окно подтверждения сообщения и кнопка сообщения В компоненты добавляем дополнительный пропс testId (если он отсутствовал), для удобного взаимодействия с ними в тестах. Изменения можно посмотреть в репозитории. Далее добавляем нажатие на соответствующие кнопки. Для этого мы использовали уже написанные разработчиками Metamask тестовые хелперы, например, waitAndTap: Пример использования в тестах: Наши тесты практически готовы. Остается приправить наш основной сценарий вышеуказанными решениями, чтобы весь цикл работы не блокировался. Баш-скрипты: Последняя задача — запустить все тесты в виде пайплайна. Для этого мы написали bash-скрипты. Так мы запустим главный (родительский) поток теста Detox, и, параллельно ему, дочерние: сканирование QR-кода и отправка запросов через API. Я выделил три необходимых скрипта: Скрипт, который запускает UI, API и Redis для хранения сессий docker-compose up -d запускает контейнеры с базой данных, а npm run all-for-tests запускает параллельно несколько npm скриптов, в данном случае запуск UI и API. Скрипт на очистку директорий от старого скриншота и запуск Cypress теста, делающего скриншот QR-кода Скрипт для запуска API тестов Эти баш скрипты будем запускать параллельно с запуском Detox. Для этого в главном тестовом файле e2e/specs/transactions-test.spec.js в проекте Metamask подключаем метод exec из модуля child_process. Он создает оболочку shell и выполняет команду, переданную в нее: Сначала запускаем скрипт exec('sh ./scripts/prepare-services.sh'), чтобы запустить API и UI. Затем exec('sh ./scripts/scan-qr-code.sh'), чтобы вырезать QR-код и вставить его на фон камеры эмулятора и отправить сессию. После этого выполняются Detox тесты для сканирования кода, тесты по созданию нового счета и так далее. Следующим этапом выполняется скрипт для запуска API-тестов — exec('sh ./scripts/run-api-tests.sh'). При этом, для всех этапов Detox тестов указаны задержки и методы, с помощью которых проверяется рендер нужных компонентов в интерфейсе (всплывающие окна, кнопки и так далее). Да, установка задержек не лучший вариант, но в данном случае, скорее всего, единственный позволяющий провести такого рода тестирование. Ниже приведен код, позволяющий с помощью Detox подтвердить запрос на подписание транзакции, созданной на стороне API: Итоги:  Теперь, помимо родительского потока Detox, взаимодействующего с пользовательским интерфейсом, параллельно в качестве дочернего процесса запускается тест для получения скриншота QR-кода. Этот поток успевает полностью выполниться до того как начнется сканирование. После сканирования QR-кода и успешного получения сессии WalletConnect, запускаются API-тесты. Здесь остаётся дождаться появления уведомлений и их одобрения. Взаимодействие с приложением и реакция на уведомления происходит в главном потоке Detox тестов. Решение не совсем очевидное и не очень элегантное. Но благодаря такому подходу мы можем совокупно запускать тесты для автоматизированной проверки всего user flow в приложениях, где совместно используются сторонние мобильные приложения и веб-сервисы. Мы залили наше решение в репозитории с инструкциями по запуску - API-сервис; тесты на скриншот и на API; тесты на Detox. Надеюсь, данная статья окажется вам полезной.