В статье покажу:

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

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

Например:

• основное приложение
• lite-версия
• отдельное приложение сервиса

Когда я начал искать оптимальное решение, нашёл много статей про настройку Appium-проекта или про написание первого теста, но вот как всё организовать, если приложений несколько, практически не нашёл. Поэтому решил поделиться своим опытом.

Не будем затрагивать боль и страдания настройки проекта. Представим, что у нас уже всё готово:

• установлен эмулятор через Android Studio
• запущен Appium Driver

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

Что будем использовать:

Java, Appium, Selenide, JUnit5, Allure, Owner (config), ADB,
Android Studio (Android 11 / API 30), Maven.

Полный код проекта доступен на GitHub: Ссылка на проект

Чтобы было проще понять архитектуру, я сделал небольшую схему.

Архитектура фреймворка
Архитектура фреймворка

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

  1. VK Видео

  2. Алхимия: Соединяй и совмещай

Задача — написать для них тесты и сделать так, чтобы проект был:

• масштабируемым
• потокобезопасным
• удобное управление двумя (а при необходимости — и большим количеством приложений)

При этом важно избежать дублирования кода.

Начнем путь от @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;
        };
    }

В этом методе фреймворк определяет, какое приложение используется в текущем тесте, и возвращает соответствующую конфигурацию.

То есть если появляется новое приложение:

  1. Добавляем его в Enum

  2. Добавляем конфигурации

После этого достаточно написать @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‑отчёты

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

В завершение хочется добавить: будьте всегда немного джунами — узнавайте новое и развивайтесь.

Спасибо, что дочитали до конца.

Буду рад фидбеку и вашим идеям по архитектуре.