Разделы портала

Онлайн-тренинги

.
Тестирование Flutter-приложений c помощью Appium
16.10.2023 12:08

Автор: Дмитрий Тумашев
Оригинальная публикация

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

С помощью стандартной библиотеки Flutter, разработчики могут создавать различные типы тестов: unit-тесты, widget-тесты и интеграционные тесты. Каждый из этих типов тестов имеет свое специфическое применение и важность.

Рассмотрим интеграционные тесты. Они проверяют, как взаимодействуют несколько частей приложения вместе, или даже весь стек приложения в целом. Так как фреймворк уже содержит в себе инструменты для написания таких тестов, Flutter-разработчики могут писать тесты, не используя дополнительных внешних инструментов. Если же тесты пишет отдельная QA-команда, это может стать проблемой, так как Dart - далеко не самый популярный язык. К счастью, есть инструменты, которые позволяют тестировать Flutter-приложения не используя Dart.

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

Appium состоит из трех основных компонентов или модулей:

  1. Сервер, который устанавливается на машине разработчика и слушает команды тестирования от клиента.

  2. Клиентские библиотеки для различных языков программирования (Java, Ruby, Python, PHP, JavaScript), которые обеспечивают поддержку для написания скриптов.

Для Flutter добавляется третий элемент - библиотека Appium-Flutter-Driver, которая позволяет тестовым сценариям взаимодействовать с Flutter-приложением.

В этом гайде описывается, как настроить окружение для тестирования с помощью Appium и создать тесты для Flutter-приложения используя Python.


Начало работы

Предположим, что у нас уже есть какое-то приложение для тестирование. В этом гайде я буду использовать маленькое тестовое приложение, которое может следующее:

  • Добавлять новые записи в список с изображением из галереи

  • Удалять добавленные записи из спиcка

  • Выполнять поиск по названию записи в Википедии, открывая страницу в WebView.



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

Весь путь этого гайда можно повторить самому - весь код хранится в репозитории appium_flutter_demo, ветка - start.

Для начала нам понадобится установленные Node и Python3, а также XCode и Android Studio. Если ваша платформа не MacOS, можете просто пропускать все части, связанные с iOS и Маком. Тестирование под Android остается идентичным.

Подготовим инструменты, которые будут выполнять тестирование приложения, а именно сам Appium и Flutter Appium Driver.

Установка Appium

Можно установить последнюю версию (appium@next), но лучше установить конкретную, которая точно корректно работает с flutter-appium-driver ( Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ).

npm i -g 
 Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript
 

Устанавливаем Appium Flutter Driver.

appium driver install --source=npm appium-flutter-driver

Настройка Flutter-проекта

Для того, чтобы Flutter-приложением можно было управлять с помощью Flutter Appium Driver, добавим в проект пакет flutter_driver.

dependencies:
  flutter:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  cupertino_icons: ^1.0.2
  file_picker: ^5.3.3
  webview_flutter: ^4.2.2

Необходимо включить расширение flutter_driver при запуске приложения. Включение расширения влияет на поведение приложения, так, например, хардверная клавиатура не будет работать, потому что её заменяет виртуальная клавиатура flutter_driver. Чтобы избежать нежелательного поведения при обычной работе с приложением в debug/profile режиме, добавим отдельную точку входа в программу.

Создадим файл main_test.dart, в нём добавим активацию расширения flutter_driver и непосредственный запуск нашего приложения.

import 'package:flutter_driver/driver_extension.dart';
import 'package:appium_demo/main.dart' as normal;

void main() {
  enableFlutterDriverExtension();
  normal.main();
}

Соберем тестовые билды. flutter_driver может работать только если приложение собрано в debug или profile режиме. Тесты у меня будут прогоняться на симуляторе iOS и эмуляторе Android. Если сборка под Android не отличается между эмулятором и реальным устройством, то у iOS всё иначе. Profile-режим недоступен для симулятора, также нужно указывать, что сборка выполняется именно под симулятор.

flutter build ios --simulator --debug -t lib/main_test.dart
flutter build apk --profile -t lib/main_test.dart

Записываем пути к созданным файлам, они еще понадобятся.

Настройка проекта с тестами

Теперь можно заняться созданием проекта с тестами. Я буду использовать Python, но Appium имеет поддержку и других ЯП. API пакетов для разных языков отличается, поэтому при необходимости писать тесты, например, на Java или JS, сверяйтесь с документацией.

Для работы с проектом установим Poetry. Вы можете использовать любой другой инструмент для управления зависимостями или вообще устанавливать пакеты через pip, их здесь немного, мне просто нравится удобство Poetry.

curl -sSL <https://install.python-poetry.org> | python3 -

Создаём проект

poetry new demo_test

Добавляем в проект зависимости для работы с Appium и Flutter Driver. Кроме того, добавим пакет dotenv, чтобы далее можно было удобно указывать конфигурацию для тестов на конкретной машине.

cd demo_test
poetry add appium-python-client appium-flutter-finder python-dotenv

Создадим файл .env в корне проекта и добавляем в него следующие значения:

# Адрес сервера Appium. По умолчанию порт - 4723.
APPIUM_HOST=http://localhost:4723
Данные об iOS устройстве для подключения
Версия iOS
IOS_PLATFORM_VERSION=16.2
Название устройства iOS
IOS_DEVICE_NAME='iPhone 14 Pro Max (430)'
Абсолютный путь к исполняемому файлу Flutter-приложения для iOS
IOS_APP_PATH=/Users/tumist/Documents/Projects/appium_demo/flutter_app/build/ios/iphonesimulator/Runner.app
Абсолютный путь к исполняемому файлу Flutter-приложения для Android
ANDROID_APP_PATH=/Users/tumist/Documents/Projects/appium_demo/flutter_app/build/app/outputs/flutter-apk/app-profile.apk

Так как приложение работает и на iOS, и на Android, сделаем тесты кроссплатформенными. Платформу выполнения тестов определит переменная окружения “PLATFORM”.

Создадим в проекте enum текущей платформы:

class Platform(Enum):
    android = 'Android'
    ios = 'iOS'

Теперь можно настроить работу Appium. Для конфигурации подключения и поведения Appium использует такое понятие, как capabilities. По своей сути это JSON, который передается серверу Appium при создании сессии. Подробнее можно почитать в официальной документации. Нас же интересуют конкретные параметры, которые нужны для корректной работы наших тестов.

  • Платформа, на которой будут выполняться тесты (platformName).

  • Название автоматизации (automationName). Для нас значение всегда 'flutter'.

  • Абсолютный путь к исполняемому файлу (app).

  • (только iOS) Название устройства, на котором будут запускаться тесты (deviceName).

  • (только iOS) Версия ОС (platformVersion).

  • (только iOS) Автоподтверждение всех диалоговых окон - запросы разрешений и т.п. (autoAcceptAlerts).

  • (только Android) Автоподтверждение всех запросов разрешений (autoGrantPermissions).

Создадим файл driver_setup.py. В нем будет происходить инициализация сессии и устанавливаться capabilities в зависимости от платформы.

load_dotenv()

def create_driver() -> webdriver.WebDriver:
    host = os.getenv('APPIUM_HOST')
    platform = Platform(os.getenv('PLATFORM'))
    capabilities = dict(
        platformName=platform.value,
        automationName='flutter',
    )
    if platform == Platform.android:
        capabilities['app'] = os.getenv('ANDROID_APP_PATH')
        capabilities['autoGrantPermissions'] = True
    if platform == Platform.ios:
        capabilities['app'] = os.getenv('IOS_APP_PATH')
        capabilities['deviceName'] = os.getenv('IOS_DEVICE_NAME')
        capabilities['platformVersion'] = os.getenv('IOS_PLATFORM_VERSION')
        capabilities['autoAcceptAlerts'] = True

    return Remote(host, capabilities)

Теперь создадим наш собственный класс для тест-кейсов, который будет наследовать питоновский unittest.TestCase. Далее наполняем его методами, которые помогут при написании тестов.

class BaseTestCase(unittest.TestCase):
    driver: WebDriver
    finder: FlutterFinder

    running_platform: Platform

    def setUp(self) -> None:
        self.driver = create_driver()
        self.finder = FlutterFinder()
        self.running_platform = Platform(os.getenv('PLATFORM'))

    def tearDown(self) -> None:
        if self.driver:
            self.driver.quit()

В setUp инициализируем драйвер, файндер Флаттера, а также определяем текущую платформу, чтобы в тестах можно было задавать разное поведение в зависимости от платформы.

В tearDown просто закрываем запущенное приложение.

Контексты приложения

Один из основных концептов в Appium - контексты. Нативная часть, Flutter, встроенные WebView - Appium использует разную логику и инструменты для работы с каждой из этих частей приложения. Именно эти части и называются контекстами.

Если мы рассматриваем Flutter-приложения, контекстов будет три:

  • FLUTTER - само Flutter-приложение или какие-то отдельные компоненты, если Flutter выборочно встраивается в нативное приложение.

  • NATIVE_APP - вся нативная часть, пикеры файлов, нативные диалоги и т.п.

  • WEBVIEW_* - WebView, встроенный в нативное или Flutter-приложение.

Для переключения между контекстами используется метод driver.switch_to.context([Идентификатор контекста]).

Для удобства работы с контекстами добавим соответствующие методы в наш класс:

def find_webview_context(driver: WebDriver):
    driver.contexts
    # Ожидание для корректной загрузки доступных контекстов
    time.sleep(3)
    return next((x for x in driver.contexts if x.startswith('WEBVIEW')), None)


def switch_to_webview(driver: WebDriver) -> None:
    webview = find_webview_context(driver)
    driver.switch_to.context(webview)


def switch_to_flutter(driver: WebDriver) -> None:
    driver.switch_to.context('FLUTTER')


def switch_to_native(driver: WebDriver) -> None:
    driver.switch_to.context('NATIVE_APP')

WebView на Android имеет идентификатор - 'WEBVIEW_[название пакета]', т.е. 'WEBVIEW_com.example.appium_demo' в нашем случае. На iOS же идентификатор динамический, устанавливаемый при создании WebView. find_webview_context как раз решает эту проблему, находя уже созданный WebView с установленным идентификатором среди всех доступных контекстов приложения.

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

Теперь поговорим о поиске элементов и взаимодействии с ними.

Работа с элементами

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

Flutter

При работе во Flutter-контексте используются методы самого Flutter Driver.

Поиск элементов выполняется с помощью метода класса FlutterFinder, например, finder.by_text([Текст, отображаемый в виджете]). Доступны следующие условия поиска:

  • by_text(string) - поиск по отображаемому в виджете тексту;

  • by_type(type_name) - поиск по типу виджета;

  • by_tooltip(string) - поиск по всплывающей подсказке;

  • descendant(finder, matching) - поиск виджета, находящегося в другом виджете;

  • и т.п.

Все доступные файндеры и команды можно посмотреть в репозитории appium-flutter-driver. Можно написать свои, что еще предстоит сделать дальше.

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

FlutterElement - это элемент, найденный по условию файндера, с которым можно как-либо взаимодействовать.

Элемент можно получить с помощью конструктора класса FlutterElement.

Натив и WebView

Если при работе с контекстом Flutter нужно отдельно создавать условие для файндера, а потом использовать конструктор FlutterElement, то при работе с нативом и вебом такого нет - нужно только использовать метод driver.find_element. В него передается стратегия поиска и искомое значение, например, driver.find_element(AppiumBy.ID, 'com.google.android.apps.photos:id/image').

Enum AppiumBy содержит доступные стратегии поиска для iOS, Android и веба. Вариантов много - идентификаторы объектов, названия классов, XPath, CSS и т.п. Разделений здесь нет, поэтому нужно понимать, какие варианты подходят для текущей платформы и контекста, а какие - нет. Посмотреть подробное описание доступных стратегий можно здесь.

Стандартные команды для элементов

Вне зависимости от того, работаем мы с Flutter, нативом или вебом, основные команды для элемента у нас одинаковые, т.к. FlutterElement наследует WebElement.

  • click() - нажатие на элемент

  • send_keys(value) - ввод текста с клавиатуры

  • clear() - очищение поля ввода

  • get_attribute(name) - получение значения атрибута элемента

Ожидание появления элементов

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

Самый банальный вариант - жесткое ожидание с помощью time.sleep([секунды]), но это будет излишне замедлять тесты при появлении элементов раньше или заставлять их падать, если элемент появится позже установленного времени ожидания. Оптимальным вариантом будет ожидание ровно до того момента, пока элемент не появится, но не дольше заданного таймаута.

Для Flutter есть команда waitFor, а для натива и веба есть класс WebDriverWait. В обоих случаях при превышении таймаута выбрасывается исключение.

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

Напишем два метода, использующих эту функциональность. Они будут возвращать bool в зависимости от наличия элемента на экране.

def is_flutter_element_exist(self, finder: str, wait_for_duration: int = 5000) -> bool:
    try:
        self.driver.execute_script('flutter:waitFor', finder, wait_for_duration)
        return True
    except WebDriverException:
        return False

def is_element_exist(
        self,
        by: str = AppiumBy.ID,
        value: str | dict | None = None,
        wait_for_duration: int = 5
):
    try:
        WebDriverWait(self.driver, wait_for_duration).until(
            expected_conditions.presence_of_element_located((by, value))
        )
        return True
    except TimeoutException:
        return False

Теперь можно написать методы получения элементов:

def get_flutter_element(self, finder: str) -> FlutterElement:
    if not self.is_flutter_element_exist(finder):
        raise Exception('Element not found')
    return FlutterElement(self.driver, finder)

def get_element(self, by: str = AppiumBy, value: str | dict | None = None) -> WebElement:
    if not self.is_element_exist(by, value):
        raise Exception('Element not found')
    return self.driver.find_element(by, value)

Благодаря этим методам в тестах мы сможем писать короткий и понятный код.

Написание тестов

Создание первого теста для Flutter

Спустя пару часов приготовлений мы наконец-то можем написать наш первый тест-кейс :)

Создадим файл с тестами test_home_screen.py.

В него добавим класс HomeScreenTest(BaseTestCase) и напишем первый кейс - открытие диалога добавления новой записи при нажатии FAB-кнопки “Добавить”. Необходимо определить параметры поиска этого элемента. Самый простой способ сделать это - узнать необходимую информацию о виджете через Flutter DevTools.

Запускаем приложение командой flutter run и переходим по ссылке на DevTools, появившейся в консоли.

An Observatory debugger and profiler on iPhone X (375) is available at: <http://127.0.0.1:55374/BBD1mV7BV9M=/>
The Flutter DevTools debugger and profiler on iPhone X (375) is available at: <http://127.0.0.1:9101?uri=http://127.0.0.1:55374/BBD1mV7BV9M=/>

В открывшемся окне браузера включаем “Select Widget Mode” и нажимаем в симуляторе на нужный нам виджет или ищем его вручную в дереве виджетов. Так, нажав на кнопку “Добавить”, искомый виджет фокусируется в дереве, и мы можем просмотреть всю необходимую информацию о нем, например, тип виджета - FloatingActionButton. Используем название типа для поиска элемента.

def test_creation_dialog_opening(self):
    """
    Если нажата 'Добавить', открывать диалог добавления новой записи
    """
    get_flutter_element(self.driver, self.finder.by_type('FloatingActionButton')).click()
    self.assertTrue(is_flutter_element_exist(self.driver, self.finder.by_type('NewItemDialog')))

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

Запустим Appium и тесты. Проследим, что наш единственный тест выполняется успешно.

appium server
poetry run python -m unittest -v

Взаимодействие с нативными элементами

Теперь напишем тест на выбор изображения из галереи. Здесь будем взаимодействовать с нативными компонентами самой ОС, и напишем сценарии для прогона и на iOS, и на Android.

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

Загружаем приложение с GitHub и запускаем. Перед нами откроется окно, где можно задать параметры соединения и необходимые capabilities. Указываем такие же параметры, как и в нашем проекте.

Нажимаем “Start session”. После этого на симуляторе должно запуститься приложение, а в окне Инспектора отобразится активная сессия. Слева теперь видим окно с приложением, в котором на данный момент будет бесконечная загрузка. Appium Inspector не поддерживает контекст Flutter, в котором запускается наше приложение, и считает, что приложение работает в нативном контексте. Сверху переключаем контекст на “Hybrid”, и обратно на “Native”, после чего приложение должно успешно отобразиться в окне.

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

С Андроидом все работает аналогично. Меняем capabilities в конфигурации, запускаем приложение на эмуляторе и открываем пикер файлов. Видим доступные варианты поиска элементов.

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

def test_add_record_picked_file(self):
    """
    Если в диалоге добавления новой записи выбран файл, на кнопке выбора отображается "Файл выбран"
    """
    self.open_creation_dialog() # Логика открытия диалога

    get_flutter_element(self.driver, self.finder.by_text('Выбрать файл')).click()
    switch_to_native(self.driver)

    if self.running_platform == Platform.ios:
        get_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Photo, August 09, 2012, 4:29 AM').click()
    if self.running_platform == Platform.android:
        get_element(self.driver, AppiumBy.ID, 'com.google.android.apps.photos:id/image').click()
        get_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Photo taken on Jul 28, 2023 11:22:49 AM').click()

    switch_to_flutter(self.driver)
    self.assertTrue(is_flutter_element_exist(self.driver, self.finder.by_text('Файл выбран')))

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

PLATFORM=Android poetry run python -m unittest -v
PLATFORM=iOS poetry run python -m unittest -v

Отметим, что здесь жестко указано, какой файл нужно выбирать в пикере. Если этот конкретный файл будет отсутствовать на устройстве, тест упадет. Вместо точных данных о элементе, можно использовать XPath или iOS Class Chain, которые позволяют, например, выбирать элемент по его порядку в списке.

Ввод текста

Пишем тест на ввод названия записи. Здесь всё просто. Открываем диалог, вводим текст в поле, проверяем наличие текста.

def test_add_record_title_edited(self):
    """
    Если в диалоге добавления новой записи введено название, оно отображается в поле
    """
    self.open_creation_dialog() # Логика открытия диалога

    field = self.get_flutter_element(self.finder.by_type('TextField'))
    field.click()
    field.send_keys(self.test_title)

    self.assertTrue(self.is_flutter_element_exist(self.finder.by_text(self.test_title)))

Создание собственного файндера

Возьмем пример с добавленной записью. Кнопка удаления записи представлена в виде одинокой иконки. Найти эту кнопку можно через файндер descendant или по типу элемента. В нашем случае сделаем именно поиск по иконке, чтобы показать, как делать собственные файндеры. Подробнее про написание файндеров и команд можно посмотреть в официальной документации Flutter.

Поиск иконок реализуем по их значению - строкам вида 'IconData(U+0F697)'. Эти значения можно найти при анализе виджетов с помощью Flutter DevTools.

Создаём файл icon_finder.dart, который будет выступать моделью данных этого файндера:

class IconFinder extends SerializableFinder {
  final String? iconData;

  const IconFinder({required this.iconData});

  @override
  String get finderType => 'IconFinder';
}

Добавляем файл icon_finder_extension.dart. В нем будет содержаться логика парсинга данных из JSON, которые будут поступать в flutter_driver из Appium, а также сама логика поиска виджета с нужной иконкой.

class IconFinderExtension extends FinderExtension {
  @override
  String get finderType => 'IconFinder';

  @override
  SerializableFinder deserialize(
    Map<String, String> params,
    DeserializeFinderFactory finderFactory,
  ) {
    return IconFinder(iconData: params['iconData']);
  }

  @override
  Finder createFinder(
    SerializableFinder finder,
    CreateFinderFactory finderFactory,
  ) {
    return find.byElementPredicate((element) {
      final iconFinder = finder as IconFinder;
      final Widget widget = element.widget;
      if (widget is Icon) {
        return (element.widget as Icon).icon.toString() == iconFinder.iconData;
      }
      return false;
    });
  }
}

Указываем это расширение в методе enableFlutterDriverExtension, чтобы flutter_driver его использовал:

void main() {
  enableFlutterDriverExtension(finders: [IconFinderExtension()]);
  normal.main();
}

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

def test_delete_record(self):
    """
    Если в карточке записи нажата кнопка "Удалить", убрать эту запись из списка
    """
    self.add_new_record() # Вся логика на добавление новой записи
    self.get_flutter_element(self.finder.by_icon('IconData(U+0F697)')).click()
    self.assertFalse(
        self.is_flutter_element_exist(
            self.finder.by_descendant(self.finder.by_type('RecordCard'), self.finder.by_text(self.test_title))
        )
    )
    self.assertTrue(self.is_flutter_element_exist(self.finder.by_type('EmptyWidget')))

Взаимодействие с WebView

Напишем тест поведения кнопки “Найти в Wikipedia” у добавленной записи. После нажатия на нее должен открываться поиск Википедии, в который уже предустановлено название записи.

В Appium Inspector есть возможность работать с WebView, но она достаточно ограничена. Даже сам Инспектор предупредит об этом следующим сообщением:

Следуем совету в сообщении и переходим на нужную страницу в Chrome вручную и открываем DevTools. Далее выбираем, по какому критерию будем искать элемент. Если ID элемента статичный, то XPath - самый простой вариант. В текущем примере используем именно его. Далее нам нужно проверить значение, установленное в строке поиска, поэтому скопируем XPath для нее - //*[@id="ooui-php-1"].

Пишем тест:

def test_record_search(self):
    """
    Если нажата кнопка поиска, открыть поиск в Википедии для названия текущей записи
    """
    self.add_new_record() # Вся логика на добавление новой записи
    self.get_flutter_element(self.finder.by_text('Найти в Wikipedia')).click()
    self.switch_to_webview()
    search_box = self.get_element(AppiumBy.XPATH, '//*[@id="ooui-php-1"]')
    self.assertEqual(search_box.get_attribute('value'), self.test_title)

Заметим, что этот тест корректно выполняется на iOS, но на Android - падает. Это происходит из-за того, что для управления WebView используется отдельное приложение Chromedriver, которое необходимо добавить в наш проект. Для этого нужно запустить Appium Server c параметрами, позволяющими автоматическую загрузку Chromedriver.

appium server --allow-insecure chromedriver_autodownload

В capabilities для Android нужно указать путь к Chromedriver, куда он будет скачиваться и откуда будет использоваться. Я добавил этот путь в .env, чтобы можно было легко его менять на разных ПК.

if platform == Platform.android:
    ...
    capabilities['chromedriverExecutableDir'] = os.getenv('CHROMEDRIVER_EXECUTABLE_DIR')

Теперь можно снова запустить тесты и удостовериться, что наш новый тест для WebView корректно выполняется. На этом написание тест-кейсов можно считать законченным.

Заключение

Мы рассмотрели, как использовать Appium для автоматического тестирования приложений на Flutter. Описанные тест-кейсы покрывают основные сценарии взаимодействия с приложением, как с Flutter-частью, так и в гибридном режиме вместе с нативом.

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

Удачного тестирования!

Обсудить в форуме