- Running Android UI Tests — Part 1
- Running tests
- Test Options
- Test example
- Artifacts
- 📘 For this blog post
- 📖 Table of Contents
- The Idea
- The flow — Prepare & Run
- Prepare
- Results
- 1. Building Execution Plan
- Build execution plan
- Run the tests
- 2. Collect Logs, Record Video, Dump DB, Shared Preferences
- Prepare
- Build execution plan
- Run the tests
- 3. Add ‘Clear data’ support
- Adding ‘ ClearData’ annotation
- Extending and printing raw file with new » AnnotationsTestPrinter»
- Build execution plan
- Run the tests
- 4. Add ‘Clear notifications’ support
- Adding “ ClearNotifications” annotation
- Build execution plan
- Run the tests
- 5. Add parameterized support
- Build execution plan
- Run the tests
- 6. Run tests by #tags
- A. Build execution plan : tags “sanity,extreme”
- B. Build execution plan : tags “small”
- Run the tests
- 7. Dump network stats, battery, alarms and more.
- Build execution plan
- Run the tests
- All together
- Автотесты на Android. Картина целиком
- Зачем нужны автотесты?
- Картина целиком
- Процесс написания тестов
- Выбор инструментов для написания автотестов
- Kaspresso
- Test runner
- На чем запускать тесты
- Инфраструктура
- Остальное
- Заключение
Running Android UI Tests — Part 1
At Medisafe 💊, we know how much our users depend and rely on our product. That is why we go the extra mile to make sure we bring them a complete, reliable solution that really works. Besides our Product and Dev teams, we do a lot of manual QA and put a lot of effort into answering our users daily through our support. Just as an example to how much coding and a quality app saves lives — read this amazing story. To top it all, we were featured both on Android and iOS apps and were recently chosen for Editor’s Choice 🎉 on Google Play.
While we improve and extend our capabilities and partnerships, we had to start writing and running automation tests. We started by covering all manual sanity scenarios. We have many. The idea (at least for now) is to run automation tests on every build and on every branch.
There are some basic things we wanted to achieve when running automated tests. We wanted no additional headaches other than working with Espresso and UIAutomator on Android side. Therefore any additional features should be simple and straightforward to apply. We wanted to be able to quickly investigate failed tests, we wanted to use only unrooted, vanilla devices.
We came up with a working flow that works perfectly for us at Medisafe and we wanted to share it with you.
Running tests
Test Options
We write UI tests same as before, when we did it manually. Only now, we can add more annotations that will give us more options:
- ClearData — Clear data via ADB before test execution.
- Repeat — Repeat the same test X number of times, when current iteration is passed to the test.
- Tags — You can tag your tests. Later, you can run tests by selected tags only.
- ClearNotifications — Clear notification bar via ADB before running the test.
Test example
Artifacts
In addition to new added options, after each failed test, we fetch and build useful files that will help us investigate the failed issues better:
- Recording (mp4)
- Logs
- DB (sqlite)
- Shared preferences
- Dumpsys — Netstats, battery, other.
📘 For this blog post
- Source code: https://github.com/medisafe/run-android-tests
- Device: Vanilla device. I am using regular Nexus 5X with Nougat.
- Android frameworks: Espresso, UIAutomator
- Script: Used for building and running execution plan. Written in bash.
📖 Table of Contents
- Building Execution Plan
- Collect Logs, Record Video, Dump DB, Shared Preferences
- Add ‘Clear data’ support
- Add ‘Clear notifications’ support
- Add parameterized support
- Run tests by #tags
- Dump network stats, battery, alarms and more.
- All together
The Idea
Instead of running all tests at once, we run them one at a time, test by test. For each test we collect logs, record a video, dump db and preferences. The main trick is to control additional tasks for every test such as clearing data by using custom annotations, InstrumentationRunListener, ADB and simple bash touch.
Note: connectAndroidTest — One might ask, why aren’t we using the connectedAndroidTest gradle task and combining it with custom Rule s. The issue is that we can’t run shell commands within a running process and user on an unrooted device. For example — “Clear data” can’t be done this way.
The flow — Prepare & Run
Prepare
- Assemble app and test APKs and install them on device.
- Use -e log true to retrieve all tests with their meta data.
- Parse the tests raw file and create ‘execution plan’ for running the tests.
- Run the executable plan line by line.
- For each non test, run appropriate ADB command.
- For each UI test start video recording, logcat.
- Execute the test.
- If test failed stop everything.
- Pull the video from device.
- Dump DB, preferences, dumpsys files.
Results
Without going into details:
- Deploy artifacts to cloud storage (S3/Drive/etc.).
- Notify on company channels (Slack/Hipchat/Trello/Jira/etc.).
- Send some CURLs to my internal webhooks to audit the results.
1. Building Execution Plan
In this chapter we will:
- Assemble and install app and test APKs.
- Fetch and print raw tests to by using -e log true flag.
- Extract the tests from raw file and build execution plan — execution-plan.txt.
- Run the tests.
Go to run-android-tests folder and from terminal, build app and test APKs:
Install both app APK and test APK on real device.
Print all available tests to the file: raw-tests.txt.
The raw.sh script doesn’t run the tests. By using -e log true flag, we print the tests only without real execution.
Build execution plan
Let’s parse this file and extract all available tests and create execution plan to execution-plan.txt
The script iterates over all lines of raw file and extracts the full name of the tests of format Class#method. Later it will also do more extractions from annotations (I will explain later).
Run the tests
Very simple, we run line by line in execution-plan.txt and execute the test. If we fail, we stop.
“FAILURES. ” — is something that is hard coded in JUnit4 — As seen on their branch. Thus we can rely on this.
2. Collect Logs, Record Video, Dump DB, Shared Preferences
Once we can run the tests from script, we have the advantage of running shell commands between the tests. This means that, before starting each test we can start Logcat and video recording processes and stop once the test is completed.
Do the same steps as before:
- Assemble and install app and test APKs
- Fetch and print raw tests to by using flag -e log true
- Extract the tests from raw file and make execution plan — execution-plan.txt
- Run the test and plan.
Prepare
Same as before. Assemble and install test APK. Create raw test file raw-tests.txt.
Build execution plan
Run the tests
The same script as in scripts/1/run.sh besides the addition before and after test execution. We start logcat process and start video recording. At the end of the test we kill both processes. Then we dump db and fetch shared preferences.
📦 The artifacts folder will contain:
3. Add ‘Clear data’ support
Originally I wanted to clear data before selected tests. Unfortunately it’s impossible from already running process on device. I had to find a working solution. This is how to clear data. We just put @ClearData annotation above the test 😉
The idea is as follow:
- We create a new annotation and call it ClearData .
- Add new AnnotationsTestPrinter that prints all annotations to the raw-tests.txt file.
- Build execution plan and add clearData command.
- Execute the plan and run adb shell pm clear package where said.
Adding ‘ ClearData’ annotation
We add new annotation under androidTest called ClearData .
Extending and printing raw file with new » AnnotationsTestPrinter»
When we print all available tests by using -e log true , we trigger built-in InstrumentationRunListener s and each of them print some metadata about the test. So, we can ride on the same logic and add our own class that will print test’s annotations as well.
Now, we need to run:
Added: -e listener com.sromku.sample.runtests.AnnotationsTestPrinter
Android JUnit Runner will run this listener per test.
The raw-tests.txt will look like this:
🔥 You can see a new line there: INSTRUMENTATION_STATUS: annotations=ClearData,Test
Build execution plan
The updated script extracts the annotations and takes ClearData into account.
The printed execution-plan.txt will look something like this:
You can see the added line clearData which means that we need to clear data at this point of time.
Run the tests
This is exactly the same script as scripts/2/run.sh other than a small additions of checking if line==clearData . If yes, clear data by using ADB command and continue to the next line in the execution plan.
4. Add ‘Clear notifications’ support
Sometimes we just want to clean the notifications bar. It can happen if I need to test that I received a new notification. But before I get one, I must be sure that the notification bar is clean of previous alerts.
All we need to do is to add ClearNotifications annotation.
The idea is exactly the same as ClearData:
- We create new annotation and call it ClearNotifications .
- We use already created AnnotationsTestPrinter .
- Build execution plan and add clearNotifications command.
- Execute the plan and clear notifications from ADB. I already wrote about the way of cleaning notifications from ADB in my other blog post.
Adding “ ClearNotifications” annotation
We add new annotation under androidTest called ClearNotifications .
Run and create raw_tests.txt:
The raw-tests.txt will look like this:
🔥 You can see a new line there: INSTRUMENTATION_STATUS: annotations=ClearNotifications,Test
Build execution plan
The updated script extracts the annotations and takes ClearNotifications into account.
The printed execution-plan.txt will look something like this:
You can see the added line clearNotifications which says that we need to clear notifications at this point of time.
Run the tests
This is exactly the same script as scripts/3/run.sh except addition checking if line==clearNotifications . If yes, clear notifications by using ADB command and continue to the next line in the execution plan.
The cleanNotifications bash command is explained here.
5. Add parameterized support
Just add @Parameterized.Repeat(count = 3) and this test will be executed 3 times with index param: 0,1,2. Get the current running index from the test through: Parameterized.getIndex() .
In this case, you can even see that I combined it with @ClearData which means that before each test iteration, we will clear app data. Cool! 😀
The raw-tests.txt will look like this:
You can see a new annotation called Repeat and number of iterations, 3, in this case.
To make this happen, we need to update AnnotationsTestPrinter . Check in the source code for full class implementation. The difference:
Build execution plan
The plan will be like this:
Now, each executable test, that should be run with param, will have an iteration number at the end of it. This number will be passed as param to the test when running.
Run the tests
This is how we pass param to the test. Taken from ./scripts/5/run.sh
And on Java side we call: Parameterized.getIndex(); The implementation of this method just takes our param from InstrumentationRegistry.
6. Run tests by #tags
Android already has an option to filter tests by:
In our case, we want filter and run tests by multiple features / tags. For example, we want choose tests that relate to “registration”, “settings”. Why? because this is the only part we have changed in our code. Or, because we had server issues in the middle of tests and we want to re-run only these sections.
In addition, we run sanity tests on every push to master. In this case, I don’t really care for the tests size and features, but run ALL tests tagged as “sanity”.
Honestly, I tried to split tests by packages, but eventually I found myself refactoring and repackaging multiple times.
And for test size, really what is ‘Small’ and what is ‘Large’? Why would I think about such splitting in a real world. If I change some area in my app, then I would run ALL tests related to this area. Just couldn’t find the use case for this.
So, eventually we wanted something more flexible. And this is how it looks: We simply add annotation: Tags(tag = <“sanity”, “small”>)
The raw-tests.txt will look like this:
New line: INSTRUMENTATION_STATUS: tags=sanity,small 🔥
To make this happen, we need to update AnnotationsTestPrinter one more time. Check in the source code for full class implementation. The difference:
buildTags — method that builds tags array as comma separated string.
A. Build execution plan : tags “sanity,extreme”
The plan will contain tests that have at least one of the filtered tags. In our case we say — Select all tests that have “sanity” or “extreme” tag.
The plan will be:
B. Build execution plan : tags “small”
The plan will be:
You can see that plans are different for different tags 🗄️.
Run the tests
7. Dump network stats, battery, alarms and more.
Once we can run any script before the test execution and after, we can add more ADB commands in a very simple way.
I added these next dumps:
Of course, you can add much more. Check for more options here. Or run: adb shell dumpsys -l for a complete list of system services.
Build execution plan
Run the tests
New artifacts added:
- dumpsys_netstats.txt
- dumpsys_batterystats.txt
- dumpsys_alarm.txt
Added lines in scripts/7/run.sh on failed test after we dumb the database.
All together
Let’s say you have test with multiple params you want to check — like Login and validation. Say you want it after cleared data, and you run this test by tag called “sanity”. In that case, this is all we need to do:
Источник
Автотесты на Android. Картина целиком
Автотесты под Android — это непросто. Чтобы выстроить процесс автотестирования, надо запланировать и решить множество задач. Но самая большая беда заключается в том, что нигде нет полного описания, что вообще включает в себя автотестирование под Android, каковы его основные стадии. Отсутствует цельная картина. Этой статьей мы хотим восполнить пробел.
Она также выступит в роли схематичной дорожной карты работы Avokado Project. Мы верим в то, что в скором времени разворачивание автотестирования будет занимать куда меньше времени, чем сейчас. И активно работаем в этом направлении.
Зачем нужны автотесты?
Есть мнение, что UI-тесты не нужны, если у вас достаточное количество юнит- и интеграционных тестов. Но от следующей метафоры никуда не деться. Представьте, что вы собираете велосипед. У вас есть два хорошо протестированных колеса, протестированная рама вместе с седлом, протестированная система педалей с цепью и рулем. То есть мы с вами имеем хороший набор интеграционных и юнит-тестов. А велосипед-то в итоге поедет? Чтобы это проверить, вы нанимаете ручных тестировщиков, которые перед каждым релизом должны убедиться, что безупречные детали корректно взаимодействуют друг с другом, и велосипед будет ездить и доставлять пользователю удовольствие.
Так же и с любым программным обеспечением для мобилок. К сожалению, в мобильном мире мы не можем откатить неудачные изменения быстро, ведь все обновления идут через Google Play Store и App Store, что сразу накладывает ограничения в виде долгой раскатки в сравнении с веб- и backend-аналогами, обязательной совместимости версий и зависимости от решения пользователя обновляться или нет. Поэтому нам критически важно всегда убеждаться перед релизом, что основные пользовательские сценарии приложения работают именно так, как ожидается.
При этом, когда релизный цикл у вас длиной в несколько месяцев, вполне достаточно работы ручных тестировщиков и некоторого покрытия кода юнит- и интеграционными тестами. Однако во времена, когда релизный цикл стремительно сокращается до одной-двух недель (а то и еще меньше), сил ручных тестировщиков зачастую уже не хватает, что вынуждает или жертвовать качеством проверки, или нанимать больше специалистов.
Все это естественным образом подводит к необходимости автоматизации проверки пользовательских сценариев, то есть написания end-to-end- или автотестов. У «Авито» есть рассказ о том, как автотесты помогают и сколько они стоят (2019 год). Однако большинство команд такой метод отпугивает своей сложностью и необходимостью вкладывать существенные ресурсы, чтобы выстроить процесс. Это возвращает нас к цели данной статьи и вообще к одной из целей Avokado Project — стандартизировать процесс автотестирования в Android и существенно уменьшить его стоимость.
Картина целиком
Итак, обещанная картина целиком.
Если вы чего-то не понимаете, не переживайте. Мы сейчас пройдемся подробно по всем пунктам.
Процесс написания тестов
На первом шаге давайте попробуем написать тесты у себя на машине и запустить их локально.
Выбор инструментов для написания автотестов
Стоит сразу определиться со стеком технологий, который мы будем использовать для написания тестов.
Первая развилка — это выбор между кроссплатформенным решением (Appium и т. д.) и нативным решением (Espresso, UI Automator). Много копий сломано в этих спорах. Рекомендуем посмотреть выступление наших коллег, полное драматизма и накала страстей.
Спойлер — мы за нативные решения. По нашему опыту, они:
- стабильнее;
- быстрее;
- лучше интегрированы в IDE;
- не содержат слоев, которые вносят нестабильность и заставляют иметь суперширокие экспертные знания.
Кроме того, Google поддерживает Espresso и UI Automator.
Больше почитать про сравнение вы можете в статьях:
На чистом Espresso и UIAutomator нынче редко кто пишет. Разработчики сделали различные удобные обертки, которые решают их проблемы. Сейчас у нас готовится статья об этих инструментах с классификацией и сравнением. Если кратко, то фреймворк, на который мы делаем ставку, это Kaspresso.
Kaspresso
Kaspresso — это фреймворк, который:
- предоставляет DSL, значительно облегчающий написание автотестов;
- дает встроенную многоуровневую защиту от флекающих тестов;
- ускоряет работу UI Automator;
- предоставляет полные логи о том, что происходит в тесте;
- дает возможность запуска любых ADB-команд внутри тестов;
- предоставляет механизм интерцепторов для перехвата всех действий и проверок. На данном механизме построено логирование и защита от нестабильных тестов;
- описывает лучшие практики (исходя из нашего опыта) по написанию автотестов.
Вы можете прочитать о Kaspresso на GitHub и Habr.
Test runner
Вы написали несколько тестов. Теперь их нужно запустить. За этот этап отвечает Test Runner, или просто раннер.
Что нам предлагает Google? Утилиту AndroidJUnitRunner и ее специальный режим — Orchestrator. AndroidJUnitRunner делает то, что от него и требуется — просто запускает тесты, позволяя еще и параллелить их выполнение. Orchestrator позволяет продолжить выполнение тестов, даже если некоторые из них упали, и дает возможность минимизировать общее состояние между тестами. Так достигается изоляция исполнения каждого теста.
Но со временем требований к раннеру становится все больше. Вот некоторые из них:
- запускать отдельные группы тестов;
- запускать тесты только на определенных девайсах;
- перезапускать упавшие тесты (вторая волна защиты от последствий нестабильных тестов после Kaspresso);
- эффективно распределять тесты по девайсам с учетом истории прогонов и успешности предыдущих запусков;
- подготавливать отчеты о прогоне в разных форматах;
- отображать результаты прогона (желательно Allure based);
- поддержать истории прогонов для дальнейшего анализа;
- просто интегрироваться с вашей инфраструктурой.
На рынке есть несколько сторонних раннеров. Среди всех них, самым перспективным мы считаем Marathon, который довольно быстро настраивается и удовлетворяет части обозначенных выше требований. Например, он поддерживает распараллеливание тестов и подготовку результатов прогона в формате, пригодном для отображения в Allure.
Однако, Marаthon, к сожалению, не обладает некоторыми важными, по нашему мнению, свойствами. В частности, в нем нет:
- Простой и нативной интеграции раннера с инфраструктурой, которая выдает эмуляторы. А еще лучше возможности сразу же запустить ваши тесты в облаке. Впрочем, это проблема не только Marathon — к сожалению, ни один известный нам раннер не берет на себя ответственность за получение эмуляторов, это всегда ложится на плечи разработчиков.
- Плотной интеграции с фреймворками типа Kaspresso для получения максимальной, точной и корректной информации о тесте.
Кроме того, мы считаем, что раннер должен быть платформенным, то есть либо для Android, либо для iOS. Это обусловлено уникальной спецификой каждой ОС и вытекающей отсюда сложностью внесения изменений в раннер.
Поэтому прямо сейчас мы работаем над Avito Runner, в котором хотим собрать все лучшие и зарекомендовавшие себя наработки и идеи. Ждите будущих анонсов!
На чем запускать тесты
Параллельно с вопросом о том, какой раннер выбрать для тестов, перед вами встает другой: а на чем лучше запускать тесты? Есть три опции:
- Настоящий девайс.
Плюсы. Покажет проблемы, специфичные для конкретных устройств и прошивок. Многие производители меняют Android под себя — как UI, так и логику работы ОС. И бывает полезно проверить, что ваше приложение корректно работает в таком окружении.
Минусы. Необходимо где-то добыть ферму устройств, организовать специальное помещение под них — необходима низкая температура, нежелательно попадание прямых солнечных лучей и т. д. Кроме того, аккумуляторы имеют свойство вздуваться и выходить из строя. А еще сами тесты могут менять состояние устройства, и вы не можете просто взять и откатиться на какой-то стабильный снепшот. - Чистый эмулятор.
Под «чистым» мы подразумеваем, что вы запускаете эмулятор у себя или где-то на машине, используя установленный на эту машину AVD Manager.
Плюсы. Быстрее, удобнее и стабильнее настоящего устройства. Создание нового эмулятора занимает считаные минуты. Никаких проблем с отдельным помещением, аккумуляторами и прочим.
Минусы. Отсутствие упомянутых выше device specifics. Однако зачастую количество тестовых сценариев, завязанных на специфику устройства, ничтожно мало, и они не высокоприоритетные. Но самый главный минус — это плохая масштабируемость. Простая задача залить новую версию эмулятора на все хосты превращается в мучение. - Docker-образ Android-эмулятора.
Docker решает недостатки чистых эмуляторов.
Плюсы. Docker и соответствующая обвязка в виде подготовки и раскатки образа эмулятора — это полноценное масштабируемое решение, позволяющее быстро и качественно готовить эмуляторы и раскатывать их на все хосты, обеспечивая их достаточную изолированность.
Минусы. Более высокий входной порог.
Мы делаем ставку на Docker.
В сети есть разные Docker-образы Android-эмуляторов, мы рекомендуем обратить внимание на следующие:
Как уже было упомянуто выше, подготовка образа требует некоторой сноровки. Плюс зачастую есть желание эмулятор преднастроить: выключить анимацию, залогиниться в аккаунт Google, выключить Google Play Protect и многое другое. Все эти вещи непросто организовать. Поэтому в скором времени мы хотим выкатить подробную документацию о том, как готовить и использовать образы быстро.
Инфраструктура
Вы написали сотни UI-тестов. Часть из них вы хотите запускать в рамках PR, а значит, весь тестовый набор должен проходить в максимально короткие сроки, например, до 20 минут. Вот тут наступает настоящее масштабирование.
Однако эта область — темный лес для части Android-разработчиков и автоматизаторов. Оно и немудрено, ведь инфраструктура требует знаний железа, конфигурирования серверов, DevOps-практик и прочего. Поэтому обязательно наладьте контакты с людьми, которые во всем этом разбираются, у себя в компании или вовне, ведь они сильно помогут и уберегут вас от ошибок.
Итак, что вам примерно предстоит:
- Выбор между облачным решением, локальным решением с нуля и локальным решением на базе чего-то, если в компании есть своя инфраструктура по запуску тестов других платформ.
- Самое трудоемкое — это развертывание внутренней инфраструктуры с нуля. В этом случае необходимо подобрать железо, которое будет максимально использоваться автотестами. Придется измерять нагрузку на CPU/GPU/Memory/Disk, а еще пробовать разное количество одновременно запущенных эмуляторов и смотреть за стабильностью тестов. Это большая тема, по которой мы хотим провести современные замеры и поделиться с вами своими рекомендациями.
Дальнейшая накатка необходимого ПО, встраивание в сети и прочее — это все за DevOps-инженерами. - На выходе должен быть какой-то сервис, единая точка, которая отдает вам эмуляторы. Это может быть Kubernetes, может быть облачный сервис типа Google Cloud Engine или какое-то кастомное решение.
В его настройке опять-таки помогают DevOps-инженеры. - Связка полученного сервиса с Test Runner.
Одна из задач Avito Runner — сделать такую связку максимально простой и прозрачной, а также предоставить точки расширения, которые помогут вам легко внедрить свой кастомный сервис.
В ближайшее время мы планируем выпустить Avito Runner и статьи, которые помогут настроить инфраструктуру.
Остальное
Не забывайте про такие немаловажные моменты, как:
- вывод отчета по прогону тестов (Allure);
- внедрение/синхронизация с TMS;
- интеграция в CI/CD;
- обучение разработчиков и тестировщиков;
- процессы — кто, когда, сколько и какие автотесты пишет.
Про все это мы еще обязательно поговорим.
Заключение
Мы постарались описать основные части становления автотестирования под Android. Надеемся, что теперь у вас в головах сложится тот самый пазл, который позволит видеть картину целиком.
Источник