Тестирование приложений через сквозные (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 с помощью бота.

  1. Создаем группу в Telegram и добавляем туда бота

  2. Выполняем все шаги для инициализации DAO

  3. Открываем UI в боте

  4. С помощью приложения Metamask сканируем сгенерированный QR-код

  5. В приложении Metamask подтверждаем запрос на подключение

  6. Выбираем нужное действие в UI бота (например, перевод эфира со счета нашего DAO на другой счет)

  7. Открываем приложение 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 мы получили ошибки. Чтобы всё правильно заработало, пришлось внести некоторые изменения в пакеты и скрипты запуска.

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

  1. Нажатие на кнопку сканирования QR-кода

  2. Ожидание сканирования QR-кода и появления уведомления о подключении

  3. Нажатие на кнопку одобрения подключения к нашему DApp

  4. Запуск API-тестов

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

  6. Нажатие на кнопку подтверждения этих транзакций

Для реализации некоторых пунктов понадобилось сделать отдельный проект. Для сканирования QR-кода пришлось делать скриншот при помощи Cypress, а на API написать тесты. Но обо всем по порядку.

Сканирование QR кода:

QR-код сканируется с помощью специального сканера внутри приложения Metamask. Этот сканер использует камеру смартфона для считывания. Но как это будет выглядеть в эмуляторе? Разработчики Android Studio сделали виртуальную сцену, как бы эмуляцию реального взаимодействия с внешним миром. Выглядит это будто в игре: вы находитесь в некой квартире, можете по ней ходить, вертеть камерой. В настройках эмулятора можно добавлять свои изображения на специальные темплейты, находящиеся в квартире, например, на доске. Это может пригодиться для сканирования QR-изображений.

Виртуальная сцена с песиком. Выглядит прикольно, не правда ли?
Виртуальная сцена с песиком. Выглядит прикольно, не правда ли?

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

  • В папке Library / Android / sdk / emulator / resources есть файл под названием Toren1BD.posters. В нем хранится информация о местоположении темплейтов в пространстве, на место которых можно вставлять свои изображения.

  • Этот файл нужно открыть и добавить туда следующие строчки:

    Добавить строчки, подсвеченные зеленым
    Добавить строчки, подсвеченные зеленым

  • Добавить изображение с названием custom.png в эту директорию. Этот png файл должен быть изображением QR-кода.

Теперь при открытии камеры у нас появляется изображение, которое мы хотим отсканировать:

Скриншот QR-кода на весь экран после открытия камеры. Удобно!
Скриншот QR-кода на весь экран после открытия камеры. Удобно!

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

const QR_CODE_SELECTOR = '.walletconnect-modal__base';
 
describe('Qr-code screenshot test', () => {
  
 it('take qr-code screenshot', () => {
   cy.visit('http://localhost:3000');
   
   cy.contains('Connect').click();
   
   cy.get(QR_CODE_SELECTOR).screenshot('qrcode');
  });
});

Скриншоты сохраняются в папке /screenshots, откуда их в дальнейшем будем доставать при помощи скриптов, переименовывать и перемещать в папку с плейсхолдерами. Код для сканирования QR-кода можно найти в репозитории.

Также, сразу после скриншота, нам бы хотелось перетащить получившееся изображение в директорию /resources. Для этого, в Cypress есть плагины, изменяющие или расширяющие его внутреннее поведение (например, эвент after:screenshot). Добавление плагинов происходит через функцию setupNodeEvents(on, config). Туда мы добавляем слушателей, которые триггерятся на заданные события и выполняют инструкции, описанные внутри. Прежде всего, напишем скрипт, который переносит скриншот в нужную нам директорию:

#!/bin/bash
 
CYPRESS_DIRECTORY="$HOME/Documents/dao-api-tests"
RESOURCES_DIRECTORY="$HOME/Library/Android/sdk/emulator/resources"
 
# move and renamed qr-code image in resource directory
cd $CYPRESS_DIRECTORY/cypress/screenshots/login.spec.cy.js
mv qrcode.png $RESOURCES_DIRECTORY/custom.png

Потом добавим функцию setupNodeEvents в файл конфигурации Cypress cypress.config.js, а в неё добавим эвент, запускающий скрипт после скриншота.

Если у вас версия Cypress ниже 10.0, то добавление плагинов происходит в файле index.ts директории plugins

const { exec } = require('child_process')
 
module.exports = defineConfig({
 
 e2e: {
   /*
   some configs
   */
   setupNodeEvents(on, config) {
       on('after:screenshot', (details) => {
       exec('sh moveScreenshot.sh');
     })
   },
 }
});

Хорошо, допустим мы отсканировали код и установили соединение. А что дальше? Следующим шагом нужно получить данные о сессии, которые получает DApp. В дальнейшем, сессия будет нужна API-сервису для отправки запросов.

Получение сессии:

Как было сказано ранее, WalletConnect устанавливает связь между двумя узлами DApp и криптокошельком пользователя (в нашем случае Metamask). Когда пользователь нажимает на кнопку “Connect” в приложении, отображается QR-код, который нужно сканировать приложением Metamask. Так происходит подключение кошелька к приложению. Metamask сканирует QR-код, расшифровывает данные запроса на соединение и показывает данные этого запроса пользователю. Далее пользователь может отклонить или одобрить установление соединения.

Уведомление на подключение приложения к Metamask после сканирования QR-кода
Уведомление на подключение приложения к Metamask после сканирования QR-кода

В случае успешного подключения, создается сессия. Благодаря сессии, Metamask сможет получать уведомления с предложением подписать или отклонить транзакцию, созданную с помощью DApp. В нашем случае требуется сохранять сессию в базе данных через вызов API метода. Это необходимо для того, чтобы при подписании транзакции отобразить запрос в приложении Metamask. Для этого сохраненная сессия используется в  провайдере, который предоставляет WalletConnect.

{
    "userId": 123456789,
    "session": {
        "connected":true,
        "accounts":["0x2D0805dB07BED54AFC3EBED54AFC3EBED54AFC3E"],
        "chainId":4,
        "bridge":"https://g.bridge.walletconnect.org",
        "key":"4aeb243c3ec2aa1739ad7def514aeba43a20ab117a354cdaf8f04aeb6e4aeb32",
        "clientId":"acd8a6e0-403a-403a-403a-17105368cc9e",
        "clientMeta":{
            "description":"DAO telegram bot",
            "url":"http://localhost:3000",
            "icons":["https://walletconnect.org/walletconnect-logo.png"],
            "name":"DAO"
        },
        "peerId":"56ea8258-6480-6480-6480-194b062944c1",
        "peerMeta":{
            "description":"MetaMask Mobile app",
            "url":"https://metamask.io",
            "icons":["https://raw.githubusercontent.com/MetaMask/brand-resources/master/SVG/metamask-fox.svg"],
            "name":"MetaMask",
            "ssl":true
        },
        "handshakeId":5678123456781234,
        "handshakeTopic":"2996df1f-6a42-6a42-6a42-78bb145cb858"
    }
}

Таким образом, при каждом новом запуске Metamask в эмуляторе для автоматизации отправки запросов нам нужно устанавливать соединение между кошельком и сервисом. То есть каждый запуск нужно сохранять новую сессию. В репозитории мы добавили простейшую реализацию UI (dao-service-stub) для отображения QR-кода. В коде можно найти обработчик события onConnect. В нем мы будем получать сессию после того как в Metamask подтверждено подключение. Далее, нам нужно передать сессию в API. Для этого вызываем эндпоинт сохранения сессии:

axios.post(`${BASE_URL}/walletConnectSessions`, connector.session, {
       headers: { 'TelegramData': JSON.stringify(telegramData) }
     });

Здесь TelegramData представляет из себя объект:

{
  "query_id":"AAE3SfY-AAAAADdJ9j4QITp1",
  "user":"{\"id\":1056327991,\"first_name\":\"Ivan\",\"last_name\":\"Ivanov\",\"username\":\"Ivanchelo\",\"language_code\":\"en\"}",
  "auth_date":"1658988403",
  "hash":"6f19e07c5fa68c540d55403bc475df4358ad04a8d532e1913260e9228241d5b3"
}

При этом стоить помнить, что API должен быть запущен. Его мы тоже запускаем с помощью скрипта перед запуском Cypress для сканирования кода. После удачного подключения сессия сохраняется в базе данных.

API-тесты:

Чтобы была возможность проверить успешность подключения по WalletConnect и работоспособность запросов, важно запустить E2E API-тесты. Их цель пройти базовый флоу от создания и инициализации DAO, до создания транзакций, взаимодействующих с Gnosis Safe. Все запросы должны сопровождаться получением уведомлений с предложением подписать или отклонить транзакцию в приложении Metamask. После подтверждения транзакция идет в блокчейн, а ее ответ приходит в наш API-сервис, за счет того же WalletConnect.

Для написания тестов на API мы выбрали Pactum JS, так как это достаточно простой, но мощный инструмент. Раннером тестов выбрали Mocha.

Пример теста на запрос, инициализирующий DAO:

it('init DAO, should takes gnosis DAO address', async () => {

  const requestBody = {
    "groupId": mockGroupId,
    "owners": mockParticipantsList,
    "threshold": 1
  };

  console.log("APROVE INITING DAO IN YOUR METAMASK APP IN 50 SEC")

  await spec()
    .post(`/groups/initDao`)
    .withHeaders('telegramData', JSON.stringify(mockPersonalTelegramData))
    .withJson(requestBody)
    .expectStatus(201)
});

По умолчанию, если ответ не был получен за 3 секунды, то любой запрос падает. Это нужно учитывать, ведь тесту, создающему транзакцию, попросту не хватит этого времени. Так что необходимо изменить значение тайм-аута до 50 секунд именно столько времени в среднем хватает на получение ответа.
Вот пример кода, который увеличивает тайм-аут запроса: 

Добавить модуль request:

const { request } = require('pactum');

Затем добавить следующую строчку в любом месте перед тестами:

request.setDefaultTimeout(50 * 1000); // 50 sec delay while user signed transaction

После отправки в тестах запроса (например, для перевода токенов) мы увидим в эмуляторе всплывающее окно с просьбой подписать сообщение:

Уведомление на подписание сообщения
Уведомление на подписание сообщения

Теперь любой тест будет ожидать ответ в течение 50 секунд. В конечном итоге мы получили сквозные API тесты, которые имитируют базовый сценарий пользователя: создание группы, инициализация DAO, создание пропозала, голосование за пропозал и др. Код тестов можно посмотреть в репозитории.

Результат работы API-тестов
Результат работы API-тестов

Теперь все блокеры можно считать разрешенными и можно переходить к нашей основной цели написанию E2E теста, в котором всё свяжется воедино.

Детокс-тесты:

Остается переделать Detox тесты под задачу. Само приложение написано на React Native, в котором используется компонентная структура. Поэтому нужно найти необходимые нам компоненты:

  • кнопка открытия камеры для сканирования QR кода

  • модальное окно подтверждения подключения и кнопка его одобрения

  • модальное окно подтверждения транзакции и кнопка его одобрения

  • модальное окно подтверждения сообщения и кнопка сообщения

В компоненты добавляем дополнительный пропс testId (если он отсутствовал), для удобного взаимодействия с ними в тестах. Изменения можно посмотреть в репозитории.

Далее добавляем нажатие на соответствующие кнопки. Для этого мы использовали уже написанные разработчиками Metamask тестовые хелперы, например, waitAndTap:

static async waitAndTap(elementId, timeout) {
   await waitFor(element(by.id(elementId)))
     .toBeVisible()
     .withTimeout(timeout || 8000);
 
   return element(by.id(elementId)).tap();
 }

Пример использования в тестах:

const SIGNATURE_CONFIRM_BUTTON = 'request-signature-confirm-button';
// it('example', () => {
  //
  //
  await TestHelpers.waitAndTap(SIGNATURE_CONFIRM_BUTTON);
// })

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

Баш-скрипты:

Последняя задача запустить все тесты в виде пайплайна. Для этого мы написали bash-скрипты. Так мы запустим главный (родительский) поток теста Detox, и, параллельно ему, дочерние: сканирование QR-кода и отправка запросов через API.

Я выделил три необходимых скрипта:

  1. Скрипт, который запускает UI, API и Redis для хранения сессий

#!/bin/bash
 
API_DIRECTORY="$HOME/Documents/dao-api-service"
METAMASK_DIRECTORY="$HOME/metamask-mobile"
# get the database up
cd $API_DIRECTORY
docker-compose up -d
 
# run UI and API
cd $METAMASK_DIRECTORY
npm run all-for-tests

docker-compose up -d запускает контейнеры с базой данных, а npm run all-for-tests запускает параллельно несколько npm скриптов, в данном случае запуск UI и API.

"run-api": "cd $HOME/Documents/dao-api-service && yarn start:dev",
"run-ui": "cd $HOME/Documents/dao-api-tests/dao-service-stub && npm start",
"all-for-tests": "npm-run-all --parallel run-ui run-api"
  1. Скрипт на очистку директорий от старого скриншота и запуск Cypress теста, делающего скриншот QR-кода

#!/bin/bash
 
QR_CODE_DIRECTORY="$HOME/Library/Android/sdk/emulator/resources"
CYPRESS_DIRECTORY="$HOME/Documents/dao-api-tests"
 
# clear qrcode in resources directory
cd $QR_CODE_DIRECTORY
rm custom.png
 
# clear qrcode in cypress directory
cd $CYPRESS_DIRECTORY/cypress/screenshots/login.spec.cy.js
rm qrcode.png
 
# make a screenshot of qrcode by cypress
cd $CYPRESS_DIRECTORY
npx cypress run --headless --browser chrome
  1. Скрипт для запуска API тестов

#!/bin/bash
 
APITEST_DIRECTORY="$HOME/Documents/dao-api-tests"
 
# run API-tests
cd $APITEST_DIRECTORY
npm test

Эти баш скрипты будем запускать параллельно с запуском Detox. Для этого в главном тестовом файле e2e/specs/transactions-test.spec.js в проекте Metamask подключаем метод exec из модуля child_process. Он создает оболочку shell и выполняет команду, переданную в нее:

import { exec } from 'child_process';

Сначала запускаем скрипт 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:

const SIGNATURE_MODAL = 'signature-modal';
const SIGNATURE_CONFIRM_BUTTON = 'request-signature-confirm-button';
const TRANSACTION_MODAL = 'txn-confirm-screen';
const TRANSACTION_CONFIRM_BUTTON = 'txn-confirm-send-button';

it('confirm and sign transactions, created by api tests', async () => {
  TestHelpers.delay(5000);
  exec('./scripts/start-api.sh'); // execute API-test
 
  // 1st of 5 transactions
  await TestHelpers.checkIfVisible(TRANSACTION_MODAL);
  await TestHelpers.waitAndTap(TRANSACTION_CONFIRM_BUTTON);
 
  // 2nd of 5 transactions
  await TestHelpers.checkIfVisible(SIGNATURE_MODAL);
  await TestHelpers.swipe(SIGNATURE_MODAL, 'up', 'fast');
  await TestHelpers.waitAndTap(SIGNATURE_CONFIRM_BUTTON);
 
  // 3rd of 5 transactions
  await TestHelpers.checkIfVisible(SIGNATURE_MODAL);
  await TestHelpers.swipe(SIGNATURE_MODAL, 'up', 'fast');
  await TestHelpers.waitAndTap(SIGNATURE_CONFIRM_BUTTON);
 
  // 4th of 5 transactions
  await TestHelpers.checkIfVisible(SIGNATURE_MODAL);
  await TestHelpers.swipe(SIGNATURE_MODAL, 'up', 'fast');
  await TestHelpers.waitAndTap(SIGNATURE_CONFIRM_BUTTON);
 
  // 5th last transactions
  await TestHelpers.checkIfVisible(TRANSACTION_MODAL);
  await TestHelpers.waitAndTap(TRANSACTION_CONFIRM_BUTTON);
  });

Итоги:

Теперь, помимо родительского потока Detox, взаимодействующего с пользовательским интерфейсом, параллельно в качестве дочернего процесса запускается тест для получения скриншота QR-кода. Этот поток успевает полностью выполниться до того как начнется сканирование. После сканирования QR-кода и успешного получения сессии WalletConnect, запускаются API-тесты. Здесь остаётся дождаться появления уведомлений и их одобрения. Взаимодействие с приложением и реакция на уведомления происходит в главном потоке Detox тестов.

Решение не совсем очевидное и не очень элегантное. Но благодаря такому подходу мы можем совокупно запускать тесты для автоматизированной проверки всего user flow в приложениях, где совместно используются сторонние мобильные приложения и веб-сервисы.

Результаты запуска тестов. Все проверки проходят в рамках одного запуска
Результаты запуска тестов. Все проверки проходят в рамках одного запуска

Мы залили наше решение в репозитории с инструкциями по запуску - API-сервис; тесты на скриншот и на API; тесты на Detox.

Надеюсь, данная статья окажется вам полезной.

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