Привет, хабровчане! С вами снова Евгений Иванов, QA-lead в компании Fix Price. В этот раз поделюсь с Вами опытом внедрения автоматизации для гибридного мобильного приложения на Android.
У этого решения есть свои плюсы и минусы, и мы продолжаем работать над его развитием. Но уже сейчас понятно: оно приносит реальную пользу команде во время регрессионных и предрелизных прогонов. Расскажу подробнее, как мы собрали связку Playwright + Appium + WebdriverIO и что из этого вышло.
Предпосылки выбора стека
В начале расскажу, почему мы пошли именно по этому пути. Наш выбор обусловлен несколькими ключевыми факторами:
Опыт команды. Наша команда уже имеет опыт написания автотестов на связке Playwright + TypeScript. Использование Playwright как основы для структуры тестовых файлов поможет быстро войти в проект мобильных тестов и снижению порога входа для новых сотрудников.
Производительность. Playwright показывает более высокую скорость выполнения по сравнению с WebdriverIO. Это делает его предпочтительным выбором для тестирования веб-части (WebView), позволяя получать результат быстрее без потери стабильности.
Управление авторизацией. Playwright умеет легко сохранять и переиспользовать состояния авторизации (cookies, localStorage). В нашем мобильном приложении на Android процессы авторизации и аутентификации выполняются именно через веб-часть SSO Keyсloak, что делает эту функцию незаменимой.
Поддержка мобильных браузеров. У Playwright есть экспериментальная поддержка для тестирования браузерных приложений на Android, что дает дополнительный инструментарий для работы с веб-контекстом.
Тестирование нативной части. Нативную часть мы тестируем через связку Appium + WebdriverIO. Синтаксис последнего во многом схож с Playwright, что позволяет поддерживать единый стиль кода в проекте.
Архитектура решения
Архитектура данного проекта значительно отличается от традиционной структуры веб-автотестов на Playwright. Мы решаем специфическую задачу: тестирование гибридного мобильного приложения, где подход к тестированию нативного Android-приложения используется совместно с тестированием веб-части и WebView. Ниже приведено подробное сравнение и описание архитектурных особенностей.(см. таблица 1)
Таблица1. Сравнение с традиционной веб-структурой Playwright.
Характеристика |
Традиционный Веб-проект (Playwright) |
Гибридный проект (PW + Appium + WDIO) |
Объект тестирования |
Браузер (Chromium, Firefox, WebKit). |
Android-устройство (Эмулятор/Реал), APK и Браузер. |
Основной драйвер |
Playwright (через протокол CDP). |
WebdriverIO (через Appium Server и UiAutomator2). |
Авторизация |
Вкладка в браузере. |
Keycloak в браузере → Перенос сессии в Android Chrome. |
Слои абстракции |
Page Object Model (POM). |
Screen Object (для native) + Page Object (для web). |
Управление сессией |
storageState используется внутри PW. |
storageState извлекается PW и инжектируется в Android Chrome через хелперы. |
Схема1.Общая схема архитектуры.

Описание ключевых слоев архитектуры.
Архитектура проекта (см. Схема1) построена по принципу разделения ответственности между шестью основными слоями. Такой подход позволяет эффективно комбинировать веб и нативные мобильные инструменты, сохраняя код поддерживаемым и понятным.
1. Слой тестов (Tests)
Setup-тесты (auth-android.setup.ts): Отвечают за предварительную авторизацию. В отличие от классических веб-тестов, здесь Playwright используется только для получения сессии (cookies, local storage), которая впоследствии будет передана на устройство.
Специфические тесты (.spec.ts): Описывают бизнес-логику и сценарии, которые выполняются непосредственно на мобильном устройстве.
2. Слой фикстур и конфигурации
Фикстура (mainScreen.fixture.ts): Объединяет в себе инициализацию WebdriverIO (remote) и объекты экранов. Служит точкой входа для подготовки окружения перед каждым тестом.
Конфиг Playwright: Управляет запуском тестов, проектами и глобальными настройками раннера.
3. Слой Page / Screen Object
LoginPage (Playwright + Web): Традиционный Page Object для работы с веб-формой авторизации через браузер.
MainScreen (WebdriverIO + Native): Использует синтаксис WebdriverIO (например, $, $$, tap) для управления элементами внутри нативного мобильного приложения.
4. Слой утилит и хелперов
Этот слой обеспечивает взаимодействие компонентов. Классы AndroidBrowserManager и HelperAuth отвечают за передачу cookies, полученных Playwright в начале теста, в браузер Chrome на Android-устройстве.
Здесь также хранится DriverData.ts — файл с настройками (capabilities) для подключения к Appium.
5. Слой инструментов
Playwright: Используется как тест-раннер и инструмент для работы с веб-контекстом.
WebdriverIO: Выступает в качестве удобной обертки (драйвера) для управления мобильной сессией Appium.
Appium: Прослойка между драйвером и устройством, которая транслирует команды в действия на устройстве.
6. Слой устройства
Конечная точка, где происходит реальное взаимодействие. Это может быть эмулятор или физический смартфон, на котором одновременно работают: браузер Chrome (с инжектированной сессией), целевое APK-приложение и системные компоненты (диалоги разрешений, уведомления).
1. Тесты.
1.1 Web-авторизация и генерация storageState.
Файл tests/auth-android.setup.ts отвечает за авторизацию в Keycloak. В нём:
открывается web-страница (Playwright),
вводятся учетные данные,
сохраняется storageState в ./support/.auth/storage.json.
tests/auth-android.setup.ts
import { test as setup, expect } from '@playwright/test'; import { LoginPage } from '@pages/LoginPage'; import { EnvHelper } from '@utilities/EnvHelper'; const adminFile = './support/.auth/storage.json'; setup('Авторизация в keycloak', async ({ page }) => { const loginPage: LoginPage = new LoginPage(page); await loginPage.open(); await loginPage.openLoginPageButton.click(); await loginPage.fillUsername(EnvHelper.login); await loginPage.fillPassword(EnvHelper.password); await loginPage.loginButton.click(); await expect(loginPage.textMyChecks).toBeVisible(); await page.context().storageState({ path: adminFile }); await page.context().close(); });
«Обратите внимание: мы не просто выполняем вход, а сохраняем состояние в storage.json. Этот файл становится ключевым артефактом — позже мы используем его на Android-устройстве для восстановления сессии. Такой подход позволяет избежать повторного прохождения процедуры логина на каждом запуске тестов.»
1.2 Пример сценария: установка, запуск и синхронизация в нативном приложении Android.
tests/android/mainScreen/installAndRunApp.spec.
import { test } from './mainScreen.fixture'; import { Data } from '@testData/data/Data'; import { expect } from '@playwright/test'; import { setupAndroidBrowserWithAuth } from '@utilities/HelperAuth'; test.describe('Проверка установки, запуска и первой синхронизации в приложении', () => { const storageState = './support/.auth/storage.json'; test.beforeAll(async ({ }) => { await setupAndroidBrowserWithAuth(storageState); }); test('Удаление, установка, запуск + вход в мобильное приложение', async ({ mainScreen, driver }) => { await driver.removeApp(Data.pkgName); await driver.installApp(Data.apkPath); await test.step('Авторизация', async () => { await driver.activateApp(Data.pkgName); await driver.switchContext('NATIVE_APP'); await mainScreen.keycloakServerInput.addValue(Data.keycloakServerUrl); await mainScreen.authKeycloakButton.tap(); }); await test.step('Запуск', async () => { await mainScreen.allowButtonDialogPermission.tap(); }); await mainScreen.snackBar.waitForDisplayed({ timeout: 20000 }); await expect.poll(async () => { return await mainScreen.snackBar.getText(); }, { timeout: 5000, intervals: [200], }).toMatch(/Синхронизация (чеклистов|задач) выполнена/); }); });
2. Фикстуры и конфигурации.
2.1 Конфигурация Playwright.
Playwright используется для:
Выполнения авторизации в web-окружении перед выполнением всех тестов;
Сохранения storageState;
Построения структуры файлов тестов и использования хуков;
Запуска всех Android-тестов в рамках общего тестового раннера;
Инжектирования cookies в браузер устройства Android.
playwright.config.ts
import { defineConfig, devices } from '@playwright/test'; import dotenv from 'dotenv'; import process from 'node:process'; import { EnvHelper } from '@utilities/EnvHelper'; dotenv.config({ path: '.env' }); export default defineConfig({ timeout: 150000, testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 0 : 0, workers: 1, reporter: 'allure-playwright', use: { baseURL: EnvHelper.baseUrl, ignoreHTTPSErrors: true, trace: 'retain-on-failure', screenshot: 'only-on-failure', actionTimeout: 30000, navigationTimeout: 30000, }, expect: { timeout: 30000, }, projects: [ { name: 'setup', testMatch: 'auth-android.setup.ts' }, { name: 'android', testMatch: '*android/*/*.spec.ts', use: { ...devices[EnvHelper.deviceName] }, dependencies: ['setup'], }, ], });
Ключевые моменты:
dependencies: ['setup'] - гарантирует, что авторизация выполнится до основных тестов;
testMatch - разделение тестов по типам (setup vs android);
EnvHelper - централизованное управление таймаутами и параметрами;
2.2 Конфигурация окружения.
.env - переменные окружения
APK_NAME_APP = FPA-debug.apk APK_NAME_CHROME = chrome.apk DEVICE_NAME1 = 'Infinix HOT 11S NFS' DEVICE_NAME = 'TECNO SPARK 30 Pro' BASE_URL = https://audit-dev3.fix-price.ru LOGIN = '******' PASSWORD = '******' APPIUM_HOST=localhost APPIUM_PORT=4723
Параметры проекта берутся из .env через support/utilities/EnvHelper.ts:
support/utilities/EnvHelper.ts
import * as process from 'node:process'; export class EnvHelper { static readonly baseUrl: string = String(process.env.BASE_URL); static readonly login: string = String(process.env.LOGIN); static readonly password: string = String(process.env.PASSWORD); static readonly apkName: string = String(process.env.APK_NAME_APP); static readonly apkNameChrome: string = String(process.env.APK_NAME_CHROME); static readonly deviceName: string = String(process.env.DEVICE_NAME); }
2.3 Фикстуры.
tests/android/mainScreen/mainScreen.fixture.ts
import { test as base } from '@playwright/test'; import { DriverData } from '@testData/data/DriverData'; import { MainScreen } from '@screens/MainScreen'; import { Browser, remote } from 'webdriverio'; type mainScreenFixtures = { driver: Browser; mainScreen: MainScreen; } export const test = base.extend<mainScreenFixtures>({ driver: async ({ }, use) => { const driver = await remote(DriverData); await use(driver); }, mainScreen: async ({ driver }, use) => { const mainScreen = new MainScreen(driver); await use(mainScreen); } });
Преимущества фикстур:
Драйвер создается один раз на тест;
Screen Object автоматически получает драйвер;
Чистая архитектура без дублирования кода;
3. Слой Page / Screen Object
support/pages/LoginPage.ts
Page Object для web-страницы авторизации:
import { Locator, Page } from '@playwright/test'; /** * Страница авторизации Keycloak */ export class LoginPage { private readonly page: Page; readonly openLoginPageButton: Locator; readonly loginButton: Locator; readonly inputUsername: Locator; readonly inputPassword: Locator; readonly textMyChecks: Locator; constructor(page: Page) { this.page = page; this.openLoginPageButton = page.getByRole('button', { name: 'Войти' }); this.loginButton = page.locator('#kc-login'); this.inputUsername = page.locator('#username'); this.inputPassword = page.locator('#password'); this.textMyChecks = page.getByText('Мои проверки'); } async open(): Promise<void> { await this.page.goto('/'); } async fillUsername(username: string) { await this.inputUsername.fill(username); } async fillPassword(password: string) { await this.inputPassword.fill(password); } }
support/screens/MainScreen.ts
Главный экран нативного приложения на Android:
import { BaseScreen } from './BaseScreen'; import { ChainablePromiseElement } from 'webdriverio'; /** * Главный экран */ export class MainScreen extends BaseScreen { readonly toolbar: ChainablePromiseElement = this.driver.$('toolbar'); readonly allowButtonDialogPermission: ChainablePromiseElement = this.driver.$('//android.widget.Button[@resource-id="com.android.permissioncontroller:id/permission_allow_button"]'); readonly buttonSync: ChainablePromiseElement = this.driver.$('//android.widget.ImageButton[@resource-id="ru.fixprice.fpa.android:id/actionSync"]'); readonly snackBar:ChainablePromiseElement = this.driver.$('//android.widget.TextView[@resource-id="ru.fixprice.fpa.android:id/snackbar_text"]'); }
Преимущества Screen/Page Object:
Изменения селекторов затрагивают только один файл;
Логика взаимодействия с экраном инкапсулирована;
Тесты читаются как бизнес-сценарии, а не работа с UI;
4. Утилиты и хелперы.
В проекте есть утилита support/utilities/AndroidBrowserManager.ts:
получает подключенное Android-устройство через Playwright Android API;
запускает Chrome на устройстве;
добавляет cookies из storageState;
import { _android, BrowserContext, AndroidDevice, BrowserContextOptions } from 'playwright'; export class AndroidBrowserManager { private device: AndroidDevice; private context: BrowserContext | undefined; constructor(device: AndroidDevice) { this.device = device; } async launchBrowser(options: BrowserContextOptions = {}): Promise<BrowserContext> { await this.device.shell('am force-stop com.android.chrome'); this.context = await this.device.launchBrowser(options); return this.context; } async saveStorageState(path: string): Promise<void> { if (!this.context) { throw new Error('Browser context is not initialized'); } await this.context.storageState({ path }); } async closeBrowser(): Promise<void> { try { if (this.context) { await this.context.close(); this.context = undefined; await this.device.shell('am force-stop com.android.chrome'); } } catch (error) { throw error; } } static async getDevice(): Promise<AndroidDevice> { const devices = await _android.devices(); if (devices.length === 0) { throw new Error('No Android devices found'); } return devices[0]; } }
support/utilities/HelperAuth.ts
Функция setupAndroidBrowserWithAuth читает storageState и внедряет cookies в браузер:
import { AndroidBrowserManager } from './AndroidBrowserManager'; import fs from 'fs'; export async function setupAndroidBrowserWithAuth(storagePath: string) { const storageState = JSON.parse(fs.readFileSync(storagePath, 'utf-8')); const device = await AndroidBrowserManager.getDevice(); const browserManager = new AndroidBrowserManager(device); const context = await browserManager.launchBrowser(); await context.addCookies(storageState.cookies); return context; }
Как это работает:
storageState читается из файла JSON;
Извлекаются cookies после web-авторизации;
Cookies добавляются в браузер на Android-устройстве;
Приложение при открытии видит авторизованную сессию;
Это позволяет не повторять логин на мобильном устройстве при каждом запуске теста.
5. Инструменты и данные по подключению.
Appium + WebdriverIO для native-части. WebdriverIO используется для управления Appium-драйвером. Данные по подключению и capabilities описаны в support/testData/data/DriverData.ts:
import { WebdriverIOConfig } from '@wdio/types/build/Capabilities'; import { Data } from '@testData/data/Data'; import { EnvHelper } from '@utilities/EnvHelper'; export const capabilitiesWithChrome = { platformName: 'Android', 'appium:automationName': 'UiAutomator2', 'appium:deviceName': EnvHelper.deviceName, 'appium:adbExecTimeout': 120000, 'appium:appWaitActivity': '*', 'appium:chromedriverAutoDownload': true, 'appium:allowInsecure': ['chromedriver_autodownload'], 'appium:chromedriverExecutableDir': './node_modules/chromedriver/bin/chromedriver', browserName: 'Chrome', 'goog:chromeOptions': { 'w3c': true, args: [ '--disable-zoom', '--force-device-scale-factor=1', '--disable-web-sockets', '--disable-bidi', '--no-first-run' ], }, }; export const capabilities = { platformName: 'Android', 'appium:automationName': 'UiAutomator2', 'appium:deviceName': EnvHelper.deviceName, 'appium:grantAllPermissions': true, 'appium:autoGrantPermissions': true, 'appium:enforceAppInstall': true, 'appium:allowTestPackages': true, 'appium:appPackage': Data.pkgName, 'appium:appActivity': Data.activity }; export const DriverData: WebdriverIOConfig = { hostname: process.env.APPIUM_HOST || 'localhost', port: parseInt(process.env.APPIUM_PORT, 10) || 4723, logLevel: 'error', capabilities, }; export const DriverDataWithChrome: WebdriverIOConfig = { hostname: process.env.APPIUM_HOST || 'localhost', port: parseInt(process.env.APPIUM_PORT, 10) || 4723, logLevel: 'error', capabilities: capabilitiesWithChrome };
Ключевые параметры:
platformName: 'Android' - платформа для тестирования;
appium:automationName: 'UiAutomator2' - движок автоматизации;
appium:grantAllPermissions - автоматическое предоставление разрешений;
appium:chromedriverAutoDownload - автозагрузка ChromeDriver;
appium:appPackage / appium:appActivity - целевое приложение;
Эти capabilities используются в фикстуре tests/android/mainScreen/mainScreen.fixture.ts, где создаётся driver и передается в screen object.
support/testData/data/Data.ts
import { EnvHelper } from '@utilities/EnvHelper'; const pkgName = 'ru.fixprice.fpa.android'; const activityName = 'ru.mobiledimension.wrs.auth.presentation.ui.AuthActivity'; export const Data = { apkPath: `support/testData/files/${EnvHelper.apkName}`, pkgName: pkgName, activity: activityName, fullName: `${pkgName}/${activityName}`, keycloakServerUrl: EnvHelper.baseUrlKC.replace('https://', ''), };
Этот файл экспортирует объект Data с тестовыми данными для Android-тестирования:
## Свойства объекта Data 1. apkPath - Значение: support/testData/files/${EnvHelper.apkName} - Назначение: Путь к APK-файлу для установки 2. pkgName - Значение: ru.fixprice.fpa.android - Назначение: Имя пакета Android-приложения 3. activity - Значение: ru.mobiledimension.wrs.auth.presentation.ui.AuthActivity - Назначение: Главный activity для экрана авторизации 4. fullName - Значение: ${pkgName}/${activityName} - Назначение: Комбинированный идентификатор package/activity 5. keycloakServerUrl - Значение: EnvHelper.baseUrlKC (без https://) - Назначение: URL сервера Keycloak для аутентификации
6. Целевые устройства и окружение.
Для запуска тестов наша инфраструктура поддерживает несколько вариантов окружения. В качестве целевого устройства Android могут выступать:
Реальные устройства: Подключаются через USB-кабель. Обеспечивают максимальную достоверность тестирования и производительность;
Локальные эмуляторы: Настраиваются через Android Studio. Удобны для быстрой отладки и не требуют физического наличия устройства;
Облачные фермы: Например, BrowserStack. Позволяют масштабировать тестирование на множество конфигураций, но требуют стабильного интернета и могут иметь задержки связи;
Docker-контейнеры: Например, budtmo/docker-android. Позволяют поднимать эмуляторы в CI/CD-среде без графического интерфейса хоста;
Каждый из этих подходов имеет свои достоинства и недостатки. На текущем этапе, чтобы обеспечить быстрый старт и гибкость отладки, мы остановились на комбинации реальных устройств и локальных эмуляторов через Android Studio
Преимущества и недостатки решения.
Любой инструмент имеет свои ограничения. Честно расскажем о плюсах и минусах нашего подхода.
Преимущества:
Единый тест-раннер. Playwright выступает основой структуры тестовых файлов и главным раннером для запуска автотестов гибридного приложения. Это позволяет держать веб и мобильные тесты в одном репозитории.
Переиспользование сессии. Состояние авторизации (localStorage, cookies) получается единожды и сохраняется в артефакт. Это значительно сокращает время запуска тестов, так как не требуется повторный логин через KeyCloak на каждом прогоне.
Единый стиль кода. Синтаксис WebdriverIO и Playwright во многом схож (особенно в структуре тестов и использовании TypeScript). Это снижает порог входа для команды и упрощает поддержку кода.
Недостатки:
Ограниченный параллелизм. Настройка workers в конфигурации Playwright отвечает за параллелизм только для веб-контекстов. Для нативных тестов через Appium параллельный запуск требует дополнительной настройки и ограничен возможностями сервера Appium.
Разный подход к ожиданиям. В отличие от Playwright, WebdriverIO использует явные ожидания (таймауты) для элементов. При написании кода для нативной части нужно не забывать об этом, чтобы избежать нестабильности тестов.
Отсутствие готовой интеграции с CI/CD. На текущем этапе проект настроен для локального запуска. Внедрение в пайплайн CI/CD запланировано на следующие итерации развития.
Ограниченная отчетность. Сейчас нет детальной визуализации результатов прогонов. Это также планируется внедрить по мере масштабирования проекта.
Заключение
Хотелось бы подчеркнуть: данное решение представляет собой экспериментальный подход к тестированию гибридного мобильного приложения на Android с использованием связки Playwright + WebdriverIO + Appium.
Ключевые преимущества подхода:
Единый тестовый фреймворк для веб и мобильной частей приложения.
Авторизация через KeyCloak с переиспользованием сессии (storageState).
Четкая структура проекта (Page/Screen Objects).
Локальный запуск тестов на реальных устройствах и эмуляторах.
Важно понимать:
Подход с использованием Playwright для работы с нативными элементами Android — нестандартное решение, которое требует глубокого понимания архитектуры.
Параллелизм тестов ограничен возможностями связки Appium и Playwright.
Архитектура может эволюционировать по мере накопления опыта и изменения требований.
Некоторые паттерны требуют дальнейшей доработки и валидации в бою.
Текущее решение — это первый шаг к автоматизации тестирования мобильного приложения. Оно демонстрирует работоспособность подхода и готово к развитию в полноценную CI/CD-систему с качественной отчетностью и масштабированием на различные устройства и конфигурации.
Но это уже темы для новых статей. Спасибо за внимание!
Пожелания от команды
Пусть ваши тесты будут:
Зелёными — как можно чаще
Быстрыми — без лишних ожиданий
Надёжными — без случайных и ложных падений
Читаемыми — чтобы коллеги говорили «спасибо»
Помните: лучшие автотесты — не те, которые покрывают 100% кода, а те, которые находят баги до продакшена и экономят время команды.