В статье покажу:
• как запускать тесты для нескольких мобильных приложений в одном Appium-проекте
• как выбирать приложение через аннотацию
• как сделать потокобезопасный фреймворк
• как избежать дублирования кода

При автоматизации мобильных приложений довольно часто возникает ситуация, когда нужно тестировать сразу несколько приложений.
Например:
• основное приложение
• lite-версия
• отдельное приложение сервиса
Когда я начал искать оптимальное решение, нашёл много статей про настройку Appium-проекта или про написание первого теста, но вот как всё организовать, если приложений несколько, практически не нашёл. Поэтому решил поделиться своим опытом.
Не будем затрагивать боль и страдания настройки проекта. Представим, что у нас уже всё готово:
• установлен эмулятор через Android Studio
• запущен Appium Driver
Покажу, как можно организовать фреймворк для мобильной автоматизации, который позволяет тестировать несколько приложений в рамках одного проекта.
Что будем использовать:
Java, Appium, Selenide, JUnit5, Allure, Owner (config), ADB,
Android Studio (Android 11 / API 30), Maven.
Полный код проекта доступен на GitHub: Ссылка на проект
Чтобы было проще понять архитектуру, я сделал небольшую схему.

Итак, у нас есть два тестируемых приложения:
Задача — написать для них тесты и сделать так, чтобы проект был:
• масштабируемым
• потокобезопасным
• удобное управление двумя (а при необходимости — и большим количеством приложений)
При этом важно избежать дублирования кода.
Начнем путь от @Test до реализации сценария.
Так выглядит один из тестов:
@MobileApp(MobileAppDictionary.VK_VIDEO) public class VkVideoMobileUITests extends BaseTest { @Description("Тест на проверку, что видео воспроизводится") @Test void checkVideoIsPlayingTest() { baseVkPage .pressBack(); vkVideoMainPage .openVideo() .videoShouldBePlaying(); }
Все элементы страницы вынесены в Page Object.
Если кратко:
• pressBack() — закрываем предложение об авторизации
• openVideo() — открываем видео для воспроизведения
• videoShouldBePlaying() — проверяем, что видео воспроизводится и завершаем тест
Мне нравится подход к декларативному написанию автотестов и здесь как раз в одном проекте используются два приложения. Очень удобно управлять всем через аннотации. В нашем случае это аннотация @MobileApp, которая ставится над тестовым классом. О ней поговорим чуть ниже. Именно она помогает решить задачу выбора приложения.
Фактически происходит следующее:
• аннотация выбирает приложение
• фреймворк сам настраивает driver
• тесту не нужно знать детали
В аннотацию мы передаём тестируемое приложение. Все приложения хранятся в Enum, который называется MobileAppDictionary. Для конфигураций и их хранения мы используем библиотеку Owner.
Ниже показан фрагмент кода, в котором в зависимости от текущего контекста выбирается конфигурация нужного приложения:
private static MobileAppConfig getCurrentAppConfig() { MobileAppDictionary app = AppContext.getApp(); if (app == null) { throw new IllegalStateException("MobileApp is not set in AppContext"); } return switch (app) { case VK_VIDEO -> vkVideoConfig; case ALCHEMY -> alchemyConfig; }; }
В этом методе фреймворк определяет, какое приложение используется в текущем тесте, и возвращает соответствующую конфигурацию.
То есть если появляется новое приложение:
Добавляем его в Enum
Добавляем конфигурации
После этого достаточно написать @MobileApp(NEW_APP) и тест будет запускаться уже для нового приложения с написанным нами тестовым сценарием и параметрами.
Наш Enum выглядит так:
public enum MobileAppDictionary { VK_VIDEO, ALCHEMY }
Ничего сложного.
Так как мы хотим запускать тесты параллельно и используем несколько приложений, нам нужен потокобезопасный фреймворк. Для этого мы реализуем класс AppContext. Для поддержки параллельного запуска используется ThreadLocal‑контекст. Детали реализации можно посмотреть в классе AppContext: ссылка
Если посмотреть в resources проекта, там можно увидеть параметры конфигурации для параллельного выполнения тестов. Это файл junit‑platform.properties, который отвечает за многопоточный запуск тестов и работает «из коробки». Подробнее можно почитать в документации: тут
Чтобы всё синхронизировать между собой, напишем свой extension-класс — AppExtension. Здесь нам нужно переопределить методы из жизненного цикла JUnit:
• BeforeEachCallback
• AfterEachCallback
И реализовать несколько важных вещей для нашего фреймворка:
• перед тестом выбираем приложение
• после теста очищаем контекст
А вот и реализация AppExtension:
public class AppExtension implements BeforeEachCallback, AfterEachCallback { @Override public void beforeEach(ExtensionContext context) { MobileApp annotation = context.getRequiredTestClass().getAnnotation(MobileApp.class); if (annotation == null) { throw new IllegalStateException("@MobileTest is not specified"); } AppContext.setApp(annotation.value()); } @Override public void afterEach(ExtensionContext context) { AppContext.clear(); } }
Чтобы использовать наш AppExtension, мы создаём собственную аннотацию @MobileApp. Внутри неё подключаем необходимые расширения через:
@ExtendWith({AppExtension.class, MobileDriverExtension.class})
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith({AppExtension.class, MobileDriverExtension.class}) public @interface MobileApp { MobileAppDictionary value(); }
На MobileDriverExtension подробно останавливаться не будем, в этом классе реализована логика:
• настройки драйвера
• скриншоты при падении тестов
• закрытие драйвера после теста
Это позволяет хранить всю логику работы с драйвером в одном месте. Полную реализацию можно посмотреть в классе MobileDriverExtension: ссылка. В целом это все, что нам потребуется.
Кстати, в фреймворке есть решение по отключению Wi‑Fi и cellular на устройстве через ADB, подробнее по ссылке InternetUtils. Использовал это для негативных тестов, чтобы проверить работу приложения без интернета. Готовое решение Airplane Mode больше не работает, начиная с Android 10 и выше.
Еще хочется добавить про подход для работы с APK, в проекте попробовал два подхода.
Первый подход
APK хранится прямо в ресурсах проекта. В этом случае используем setApp( путь до ресурсов, где хранится наше приложение ) вместе с setAppPackage("com.vk.vkvideo"). При таком подходе setAppActivity не требуется (По дефолту процесс начнется с основного экрана "Главной Activity"), так как Appium сам определяет необходимые параметры.
Второй подход
APK устанавливается напрямую на устройство (или эмулятор, как у нас). В этом случае мы не используем setApp(...), а передаём setAppActivity(...) и setAppPackage("com.vk.vkvideo"). Дальше фреймворк сам определяет, что нужно использовать в зависимости от контекста, а мы думаем только о тесте и нашем сценарии.
Справедливости ради добавлю, что при тестировании приложения setAppPackage("com.ilyin.alchemy") у меня зависал экран ( см. скрин из Allure ниже ) при нажатии на подсказки, возможно я скачал версию apk с багом, так как скачивал с APKMirror, если будете экспериментировать и у вас все заработает — делитесь.
Скрин из Allure Reports:

В результате получился небольшой, но масштабируемый Mobile Automation Framework, который позволяет:
• тестировать несколько приложений
• запускать тесты параллельно
• автоматически управлять драйвером
• удобно подключать новые приложения
• отключать и включать интернет на устройстве
• делать screenshot при падении тестов
• формировать Allure‑отчёты
Этот фреймворк появился просто, как эксперимент. Но в процессе, на мой взгляд, получилась удобная архитектура, которая позволяет тестировать несколько мобильных приложений в одном проекте.
В завершение хочется добавить: будьте всегда немного джунами — узнавайте новое и развивайтесь.
Спасибо, что дочитали до конца.
Буду рад фидбеку и вашим идеям по архитектуре.
