В прошлой статье я рассказывал, что для ускорения проверки релизов мы используем Appium. Ниже расскажу, как мы настроили инфраструктуру, способную прогонять более 5 тысяч тестов в сутки на iOS и Android суммарно. Секрет прост — использовать Selenoid. Об этом и расскажу под катом.
Предисловие
В прошлом году на Хабре вышла статья по исследованию автотестов, в рамках которой мы приняли участие в опросе.
Результаты опроса нас порадовали, поэтому мы решили написать статью, поделиться опытом с вами и получить советы. Материал решили разделить на две части: в первой части расскажем про Android, во второй — про iOS.
Начнём с Android.
Запуск тестов на Android
Selenoid — популярный инструмент, который позволяет запускать браузеры и Android-эмуляторы в Docker контейнере и управлять ими. Подробнее можно прочитать в документации или в статьях на Хабре: раз, два и три.
Для написания тестов на Appium мы используем:
Kotlin;
JUnit 5;
Maven.
Первый запуск. Настройка Selenoid
Создаём конфиг
browsers.json
:
{
"android": {
"default": "10.0",
"versions": {
"10.0": {
"image": "browsers/android:10.0",
"port": "4444",
"path": "/wd/hub"
}
}
}
}
В image
указывается образ эмулятора. Ребята из aerokube подготовили готовые образы Android-эмуляторов. Их можно посмотреть тут или тут. Образы ничем не отличаются.
Для примера выберем образ browsers/android:10.0
. Образ надо заранее скачать: docker pull browsers/android:10.0
, иначе тесты не запустятся:
Original error: create container: Error response from daemon: No such image: browsers/android:10.0
Следующим шагом запускаем Selenoid. Делаем напрямую через docker, есть возможность запуска через Configuration Manager.
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/selenoid/config/":/etc/selenoid/:ro \
-p 4444:4444 \
--name selenoid \
aerokube/selenoid:1.10.7
Работоспособность Selenoid можно проверить через ссылку в браузере http://localhost:4444:
You are using Selenoid 1.10.7!
Указываем адрес Selenoid в Appium-тестах в драйвере:
...
val driver = AndroidDriver(URL("http://localhost:4444/wd/hub"), capabilities)
...
Дальше указываем ссылку на сборку в
capabilities
:
...
capabilities.setCapability("appium:app", "https://storage.example.com/builds/app.apk")
...
Если нет возможности указать ссылку, можно указать путь до сборки:
...
capabilities.setCapability("appium:app", "/builds/app.apk")
...
Где /builds/app.apk
— путь внутри контейнера, где запускается эмулятор. Чтобы вариант работал, надо указать volumes
в browsers.json
:
{
"android": {
"default": "10.0",
"versions": {
"10.0": {
...
"volumes": [
"/home/username/app.apk:/builds/app.apk:ro"
]
...
}
}
}
}
Где /home/username/app.apk
— путь до сборки на хосте.
Всё, мы почти настроили Selenoid и можем попробовать запустить тесты:
./mvnw test
Но, к сожалению, тесты не смогут запуститься. Идём разбираться.
Второй запуск. Смотрим логи и видеозапись
Первое, что надо сделать после неудачного запуска — посмотреть логи Selenoid:
docker logs selenoid
[INIT] [Loading configuration files...]
[INIT] [Loaded configuration from /etc/selenoid/browsers.json]
[INIT] [Video Dir: /opt/selenoid/video]
[INIT] [Your Docker API version is 1.41]
[INIT] [Timezone: UTC]
[INIT] [Listening on :4444]
[NEW_REQUEST] [unknown] [172.17.0.1]
[NEW_REQUEST_ACCEPTED] [unknown] [172.17.0.1]
[LOCATING_SERVICE] [android] [10.0]
[USING_DOCKER] [android] [10.0]
[CREATING_CONTAINER] [selenoid/android:10.0]
[STARTING_CONTAINER] [selenoid/android:10.0] [75e454341da7fc4b58ba104a5180813bac6cd7c124037a759b6c976e65b168fa]
[CONTAINER_STARTED] [selenoid/android:10.0] [75e454341da7fc4b58ba104a5180813bac6cd7c124037a759b6c976e65b168fa] [0.40s]
[0] [REMOVING_CONTAINER] [75e454341da7fc4b58ba104a5180813bac6cd7c124037a759b6c976e65b168fa]
[0] [CONTAINER_REMOVED] [75e454341da7fc4b58ba104a5180813bac6cd7c124037a759b6c976e65b168fa]
[0] [SERVICE_STARTUP_FAILED] [http://172.17.0.3:4444/wd/hub does not respond in 30s]
Видим статус SERVICE_STARTUP_FAILED. Идём в документацию и смотрим значение статуса:
SERVICE_STARTUP_FAILED - Failed to start Docker container or driver binary
Ошибка ничего не говорит, нужно больше информации. Хорошо бы посмотреть логи контейнера. Так и сделаем — для этого включим логирование:
docker run -d \
-p 4444:4444 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/selenoid/config/":/etc/selenoid/:ro \
-v "$(pwd)/selenoid/logs/":/opt/selenoid/logs/ \
aerokube/selenoid:1.10.7 \
-log-output-dir /opt/selenoid/logs
Также включаем логи в capabilities:
...
capabilities.setCapability("enableLog", true)
...
Запускаем тесты и смотрим логи через браузер http://localhost:4444/logs/:
Логи контейнера
2023-04-16T13:44:43.909768530Z Waiting X server...
2023-04-16T13:44:44.009494775Z Logging to: /tmp/fluxbox.log
2023-04-16T13:44:44.047587277Z Waiting X server...
2023-04-16T13:44:44.151933325Z Waiting X server...
2023-04-16T13:44:44.262850410Z * daemon not running; starting now at tcp:5037
2023-04-16T13:44:44.457972956Z * daemon started successfully
2023-04-16T13:44:44.458249266Z adb: no devices/emulators found
2023-04-16T13:44:45.463480812Z adb: no devices/emulators found
2023-04-16T13:44:46.471547723Z adb: no devices/emulators found
2023-04-16T13:44:47.476093515Z adb: no devices/emulators found
2023-04-16T13:44:48.481987351Z adb: no devices/emulators found
2023-04-16T13:44:49.486503149Z adb: no devices/emulators found
2023-04-16T13:44:50.492757801Z adb: no devices/emulators found
2023-04-16T13:44:51.499094108Z adb: no devices/emulators found
2023-04-16T13:44:52.505862428Z adb: no devices/emulators found
2023-04-16T13:44:53.513276412Z adb: no devices/emulators found
2023-04-16T13:44:54.520642210Z adb: no devices/emulators found
2023-04-16T13:44:55.527420189Z adb: no devices/emulators found
2023-04-16T13:44:56.534631013Z adb: no devices/emulators found
2023-04-16T13:44:57.316094939Z WARNING. Using fallback path for the emulator registration directory.
2023-04-16T13:44:57.335415397Z checkValid: hw configs not eq
2023-04-16T13:44:57.541959741Z adb: device offline
2023-04-16T13:44:58.547907700Z adb: device offline
2023-04-16T13:44:58.565504866Z emulator: WARNING: System image is writable
2023-04-16T13:44:58.565528396Z emulator: Cold boot: different AVD configuration
2023-04-16T13:44:58.565532576Z Your emulator is out of date, please update by launching Android Studio:
2023-04-16T13:44:58.565536346Z - Start Android Studio
2023-04-16T13:44:58.565539506Z - Select menu "Tools > Android > SDK Manager"
2023-04-16T13:44:58.565543076Z - Click "SDK Tools" tab
2023-04-16T13:44:58.565546216Z - Check "Android Emulator" checkbox
2023-04-16T13:44:58.565549216Z - Click "OK"
2023-04-16T13:44:58.565552146Z
2023-04-16T13:44:59.554451514Z adb: device offline
2023-04-16T13:45:00.560926060Z adb: device offline
2023-04-16T13:45:01.568777440Z adb: device offline
2023-04-16T13:45:12.124226047Z emulator: INFO: boot completed
2023-04-16T13:45:12.124251007Z emulator: INFO: boot time 27848 ms
2023-04-16T13:45:12.124255077Z emulator: Increasing screen off timeout, logcat buffer size to 2M.
2023-04-16T13:45:12.152557294Z emulator: Revoking microphone permissions for Google App.
Логи контейнера тоже ничего не говорят, потому что не видно логов Appium. Давайте попробуем их включить. Для этого посмотрим на скрипт entrypoint.sh:
...
if [ -z "$VERBOSE" ]; then
APPIUM_ARGS="$APPIUM_ARGS --log-level error"
else
EMULATOR_ARGS="$EMULATOR_ARGS -verbose"
fi
...
Чтобы включить логи Appium, надо передать в контейнер параметры VERBOSE=true
и APPIUM_ARGS=--log-level debug
:
{
"android": {
"default": "10.0",
"versions": {
"10.0": {
...
"env": [
"VERBOSE=true",
"APPIUM_ARGS=--log-level debug"
]
...
}
}
}
}
Тут есть одна проблема
Чтобы включить дебажные логи Appium, необходимо передавать VERBOSE, а в этом случае включаются логи эмулятора и начинают заполнять «эфир». Но мы это исправили для будущих образов =)
Теперь достаточно передавать в контейнерAPPIUM_ARGS=-log-level debug
Логи контейнера с логами Appium
...
[HTTP] --> POST /wd/hub/session/c89fa9c2-ca2b-49cd-ab31-590eeccf77d1/element
[HTTP] {"using":"id","value":"authorization_edittext_phone"}
[debug] [W3C (c89fa9c2)] Calling AppiumDriver.findElement() with args: ["id","authorization_edittext_phone","
c89fa9c2-ca2b-49cd-ab31-590eeccf77d1"]
[debug] [BaseDriver] Valid locator strategies for this request: xpath, id, class name, accessibility id, -and
roid uiautomator
[debug] [BaseDriver] Waiting up to 0 ms for condition
[debug] [WD Proxy] Matched '/element' to command name 'findElement'
[debug] [WD Proxy] Proxying [POST /element] to [POST http://127.0.0.1:8200/wd/hub/session/65943f03-3b35-4d3eb221-d6dc7988f935/element] with body: {"strategy":"id","selector":
"authorization_edittext_phone","context":"","multiple":false}
[WD Proxy] Got response with status 404: {"sessionId":"65943f03-3b35-4d3e-b221-d6dc7988f935","value":{"error"
:"no such element","message":"An element could not be located on the page using the given search parameters","stacktrace":"io.appium.uiautomator2.common.exceptions.El
ementNotFoundException: An element could not be located on the page using the given search parameters\n\tat io.appium.uiautomator2.handler.FindElement.safeHandle(Find
Element.java:73)\n\tat io.appium.uiautomator2.handler.request.SafeRequestHandler.handle(SafeRequestHandler.java:41)\n\tat io.appium.uiautomator2.server.AppiumServlet.
handleRequest(AppiumServlet.java:253)\n\tat io.appium.uiautomator2.server.AppiumServlet.handleHttpRequest(AppiumServlet.java:247)\n\tat io.appium.uiautomator2.http.Se
rverHandler.channelRead(ServerHandler.java:68)\n\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:366)\n\tat io
.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:352)\n\tat io.netty.chann...
[debug] [W3C] Matched W3C error code 'no such element' to NoSuchElementError
[debug] [W3C (c89fa9c2)] Encountered internal error running command: NoSuchElementError: An element could not
be located on the page using the given search parameters.
[debug] [W3C (c89fa9c2)] at AndroidUiautomator2Driver.findElOrEls (/opt/node_modules/appium/node_modules/appium-android-driver/lib/commands/find.js:75:11)
[debug] [W3C (c89fa9c2)] at process._tickCallback (internal/process/next_tick.js:68:7)
[HTTP] <-- POST /wd/hub/session/c89fa9c2-ca2b-49cd-ab31-590eeccf77d1/element 404 23 ms - 444
...
Уже чуть больше информации. По логам видно, что Appium не может найти наш элемент. Посмотрим, что происходит на экране эмулятора. Для этого надо запустить Selenoid UI:
docker run -d \
--name selenoid-ui \
-p 8080:8080 \
--link selenoid:selenoid \
aerokube/selenoid-ui:1.10.4 \
--selenoid-uri "http://selenoid:4444"
Переходим по ссылке http://0.0.0.0:8080 и открываем Selenoid UI:
Не забудем включить в тестах VNC и видеозапись:
...
capabilities.setCapability("enableVNC", true)
capabilities.setCapability("enableVideo", true)
...
В итоге команда запуска Selenoid выглядит так:
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "(pwd)/selenoid/logs/":/opt/selenoid/logs/ \
-v /opt/selenoid/video/:/opt/selenoid/video/ \
-e OVERRIDE_VIDEO_OUTPUT_DIR="/opt/selenoid/video/" \
-p 4444:4444 \
-name selenoid \
aerokube/selenoid:1.10.7 \
-log-output-dir /opt/selenoid/logs
После запуска тестов открываем Selenoid UI:
А вот и видеозапись:
Ура, мы нашли причину ошибки запуска. Идём дальше.
Третий запуск. Собираем образ эмулятора
Как оказалось, у образов Selenoid нету сервисов Google Play. Чтобы исправить это, надо самому собрать образ эмулятора. Ребята из aerokube подготовили для этого все необходимое: репозиторий с образами и документацию.
Скачиваем репозиторий.
Переходим в папку
selenium
.Запускаем скрипт
./automate_android.sh
и отвечаем на вопросы. В нашем случае это выглядит так:
Specify Appium version: [1.18.1]
>> 1.18.1
Specify Android image type (possible values: "default", "google_apis", "google_apis_playstore", "android-tv", "android-wear"): [default]
>> google_apis
Specify Application Binary Interface (possible values: "armeabi-v7a", "arm64-v8a", "x86", "x86_64"): [x86]
>> x86
Specify Android version: [8.1]
>> 10.0
Specify device preset name if needed (e.g. "Nexus 4"):
>>
Specify SD card size, Mb: [500]
>>
Specify userdata.img size, Mb: [500]
>>
Are you building a Chrome Mobile image (for mobile web testing): [n]
>> y
Specify Chromedriver version if needed (required for Chrome Mobile):
>> 74.0.3729.6
Specify image tag: [selenoid/chrome-mobile:74.0]
>> android-emulator:10.0
Add Android quick boot snapshot? [y]
>> n
Подробнее про Add Android quick boot snapshot?
Когда увидел этот вопрос, то подумал, что это snapshot эмуляторов. Но если посмотреть код, то там вызывается скрипт, который нужен для установки APK приложений. По сути, это не даёт нам какого-либо выигрыша.
Если говорить про ускорение запуска контейнера, мы уже используем snapshot эмуляторов, но об этом расскажем в других статьях.
4. После того, как образ соберётся, появится предложение запушить его в registry. Нам пока это не нужно, поэтому любезно откажем:
Push?
>> n
Мы собрали сборку с сервисами Google Play. Не забываем поменять параметр image
в browsers.json
и перезапустить Selenoid.
Попробуем ещё раз запустить тесты:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
А вот и видеозапись прогона:
Итог
Что мы сделали:
Настроили Selenoid для запуска Android-тестов.
Научились смотреть логи и видеозаписи для поиска проблемы.
Собрали свой образ эмулятора c сервисами Google Play.
О чём хотелось бы рассказать ещё:
О таймаутах Selenoid. Если у вас большое приложение, можно столкнуться с проблемами, связанными с таймаутами.
Как мы ускоряли запуск контейнера. На видеозаписи можно увидеть, что на запуск тратится примерно минута.
Как мы пробовали запустить нативные тесты Android, написанные на Espresso, на Selenoid. Спойлер: это работает!
А пока в следующей части расскажем, как мы масштабировали инфраструктуру и запускали тесты на iOS.