Тестирование мобильных приложений на Flutter с использованием Python + Appium
- вторник, 20 декабря 2022 г. в 00:47:15
Тестирование нативных мобильных приложений для Android и iOS обычно обеспечивается библиотеками UiAutomator2 и XCUITest, но сейчас, в связи с появлением мобильных приложений, разработанных с использованием Flutter Framework, использовать их для тестирования становится затруднительно, поскольку семантическая информация, публикуемая Flutter, в большинстве случаев недостаточна для однозначной идентификации виджетов и взаимодействия с ними. В этой статье мы рассмотрим возможности драйвера appium-flutter-driver для тестирования flutter-приложений, разберемся с использованием виджетов описания семантики и разработаем несложные тесты на Python с использованием Appium.
Если мы используем Appium 2 (appium@next), то нас доступна установка драйвера как расширения appium.
appium driver install --source=npm appium-flutter-driver
при использовании Appium 1 драйвер устанавливается как отдельный пакет в системе:
npm i -g appium-flutter-driver
Для корректного выполнения тестов нужно включить расширение FlutterDriverExtension в Flutter-приложении и собрать приложение в режиме отладки или профилирования. Создадим простое приложение на Flutter и последовательно разберемся, какие селекторы будут необходимы для выбора виджета на экране, как отправлять команды (действия) виджету, как проверять корректность изменения значений или видимости виджетов. За основу возьмем шаблонное приложение счетчика и добавим к нему необходимую семантическую информацию. Создадим проект командой flutter create (у вас должен быть установлен Flutter SDK версии 3.0 или выше, получить его можно по ссылке):
flutter create counter
cd counter
Для поддержки тестов добавим в pubspec.yaml зависимость flutter_driver
в dev_dependencies
. Установим зависимости через flutter pub get
.
flutter_driver:
sdk: flutter
и разрешим использование расширения перед запуском Flutter-приложения (в main):
import 'package:flutter_driver/driver_extension.dart';
void main() {
enableFlutterDriverExtension();
runApp(const MyApp());
}
Дополнительно можно определять реализации классов FinderExtension (для поиска виджетов по особым признакам) и CommandExtension (для выполнения дополнительных действий с виджетами или иными объектами). Соберем наше приложение в debug apk (для Android) или ipa (для iOS, в этом случае для запуска будет использовать Runner.zip):
flutter build apk --debug
mv build/app/outputs/flutter-apk/app-debug.apk /tmp
Для запуска приложения в режиме отладки или профилирования (с расширением оно будет доступно для внешнего управления) создадим реализацию автоматического теста на Appium с использованием драйвера flutter.
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
caps = {}
caps["platformName"] = "Android"
caps["appium:app"] = "/tmp/app-debug.apk"
caps["appium:automationName"] = "flutter"
caps["appium:retryBackoffTime"] = 500
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
# test
driver.quit()
Если при запуске возникает ошибка, то это может быть связано с тем, что Appium Flutter Driver не смог найти observatory URL. В этом случае можно запустить приложение вручную, скопировать адрес из Debug service listening (ws://…) и подключиться к существующему приложению:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
caps = {}
caps["appium:observatoryWsUri"] = "ws://127.0.0.1:61985/D2nsixa5UkE=/ws"
caps["platformName"] = "Android"
caps["appium:automationName"] = "flutter"
caps["appium:retryBackoffTime"] = 500
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
# test
driver.quit()
Если при запуске возникает ошибка, то это может быть связано с тем, что Appium Flutter Driver не смог найти observatory URL. В этом случае можно запустить приложение вручную, скопировать адрес из Debug service listening (ws://…) и подключиться к существующему приложению:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
caps = {}
caps["appium:observatoryWsUri"] = "ws://127.0.0.1:61985/D2nsixa5UkE=/ws"
caps["platformName"] = "Android"
caps["appium:automationName"] = "flutter"
caps["appium:retryBackoffTime"] = 500
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
# test
driver.quit()
Flutter-приложение будет доступно как гибридное приложение, в котором контекст FLUTTER будет предоставлять возможности обнаружения элементов по Flutter-ключам (например, ValueKey) и типам виджетов, NATIVE_APP - поиск нативных View для Android/iOS, либо могут быть доступны дополнительные контексты для WebView.
Для поиска элементов во Flutter в Python можно использовать следующие методы (из модуля appium_flutter_finder.flutter_finder (объект класса FlutterFinder):
by_text(string)
- поиск по отображаемому тексту
by_tooltip (string)
- по всплывающей подсказке
by_ancestor(finder, matching)
- поиск среди родителей обнаруженных в finder виджетов
by_descendart(finder, matching)
- поиск среди потомков обнаруженных в finder виджетов
by_value_key(string)
- по ключу (при создании виджета указывается как именованный параметр конструктора key: ValueKey(‘string’)
by_semantics_label(string)
- по семантической метке (может быть добавлена через атрибут semanticsLabel у стандартных виджетов или виджет-обертку Semantics(label: ‘метка’, child: …)
by_type(string)
- поиск по типу (названию класса виджета)
page_back()
- возврат на предыдущую страницу
Для обнаруженного элемента (FlutterElement, наследуется от WebElement) можно использовать стандартные действия Appium:
click()
- выполнить однократное нажатие на элемент
driver.execute('flutter:longTap’, element, {'durationInMilliseconds’:time})
- выполнить долгое нажатие на виджет
clear()
- очистить ранее введенный текст
send_keys(string)
- ввести текст
is_selected(), is_enabled(), is_displayed()
- проверить состояние виджета
text
- получить текущее значение (для текстовых виджетов)
Также можно использовать W3C Actions для описания произвольных жестов (через цепочку действий с указателем). Дополнительные возможности также доступны через выполнение команд (driver.execute_command):
flutter:forceGC
- выполнить сборку мусора в Dart
flutter:getBottomLeft
, flutter:getBottomRight
, flutter:getTopLeft
, flutter:getTopRight
, flutter: flutter:getCenter
- получить координаты точек для указанного FlutterElement
(в аргументе)
flutter:getRenderTree
- возвращает дерево Render Objects
flutter:getSemanticsId
- получить id из Semantics для указанного FlutterElement (в аргументе)
flutter:scroll
- выполнить прокрутку списка или любого прокручиваемого виджета (SingleChildScrollView
, CustomScrollView
), первым аргументом указывается FlutterElement, вторым - словарь с конфигурацией (dx, dy, durationMillisconds)
flutter:scrollIntoView
- прокрутить экран до отображения указанного элемента
flutter:scrollUntilVisible
- прокрутить до появления элемента на экране
flutter:scrollUntilTapable
- прокрутить до активации возможности нажатия на элемент
Через команды также доступна синхронизация теста и изменений интерфейса:
flutter:waitFor
- ожидание появления виджета (FlutterElement
указывается в аргументе)
flutter:waitForAbsent
- ожидание исчезновения указанного виджета
flutter:waitForTapable
- ожидание возможности нажать на виджет
flutter:waitForFirstFrame
- ожидание отображения первого кадра
Для создания скриншота можно использовать driver.save_screenshot(filename)
. Дополнительно можно взаимодействовать с Dart VM и запрашивать информацию о текущем состоянии driver.execute('flutter:getVMInfo')
, получать текущий изолят driver.execute('flutter:getIsolate')
.
Для нашего простого приложения тест может выглядеть следующим образом:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium_flutter_finder.flutter_finder import FlutterElement, FlutterFinder
caps = {}
caps["appium:observatoryWsUri"] = "ws://127.0.0.1:63079/c5ODoTGB0Wg=/ws"
# caps["appium:appPackage"] = "com.example.counter" # можно посмотреть в android/app/src/main/AndroidManifest.xml
# caps["appium:appActivity"] = "MainActivity"
caps["appium:noReset"] = True
caps["platformName"] = "Android"
caps["appium:automationName"] = "flutter"
caps["appium:retryBackoffTime"] = 500
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
driver.execute("flutter:waitForFirstFrame")
finder = FlutterFinder()
counter = finder.by_value_key("Counter") # нужно добавить key: ValueKey('Counter') к виджету счетчика нажатий
assert (counter.text == '0')
fab: FlutterElement = finder.by_type("FloatingActionButton")
fab.click()
assert (counter.text == '1')
driver.quit()
Использование драйвера appium-flutter-driver позволяет выполнять автоматическое тестирование flutter-приложений и создавать отчеты о тестировании через привычный инструмент тестирования мобильных приложений Appium.
В заключение приглашаю всех желающих на бесплатный урок, где мы познакомимся с системой автоматизации развёртывания и управления приложениями Docker, посмотрим как использовать некоторые базовые команды Docker CLI и попробуем "упаковать" тесты в Docker-контейнер.