Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Автотесты под Android - так ли это просто?

Автотесты под Android - так ли это просто?

Очень многие сегодня хотят внедрить автотесты в свои проекты. Многие неправильно оценивают сложность внедрения, расценивая эту задачу как простую. Однако приходит суровая реальность и на автотесты приходится потратить намного больше времени, чем ожидалось. Так в чем же дело? Давайте разберемся вместе на моем докладе.

Dmitriy Movchan

August 09, 2019
Tweet

More Decks by Dmitriy Movchan

Other Decks in Technology

Transcript

  1. Дмитрий Мовчан 2 •Android разработчик в Revolut •Android Academy MSK

    организатор •Спикер Mobius, Appsconf и другие…
  2. Так, опять автотесты??? 4 Все хотят выпускать фичи как можно

    быстрее Что для этого нужно? Один из пунктов это как можно быстрее убедиться в том, что продукт требуемого качества Как это сделать?
  3. Так в чем же проблема? 7 Проблемы: - в индустрии

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

    - сбережет вам кучу нервов - даст вам базу, с которой вы сможете просто начать писать автотесты
  5. !9

  6. A long time ago in a galaxy far far.. 1

    1 2015 2016 2017 2018 2019
  7. 12 Espresso. Первая кровь …провал из-за: • Сложности поддержки •

    Слабых инструментов и отсутствия каких-либо практик A long time ago in a galaxy far far..
  8. 14 A long time ago in a galaxy far far..

    Appium Отдельная команда автотестеров (по сути тестировщики и стажеры) Отдельный проект автотестов Язык написания автотестов - Ruby
  9. In 05.2018 15 Релизы были крайне редко Регресс длился 21

    ч/д Комьюнити начинало говорить о пользе автотестов • Подкасты • Конференции • Статьи • Инструменты
  10. Что переосмыслять? 16 • Тесты должны быть не только black

    box • Тесты должны быть вместе с проектом • Разработчики должны драйвить написание автотестов
  11. Как это обычно происходит 18 К вам приходит менеджер "

    " ставит вам задачу автоматизировать ручное тестирование Вы разработчик # # представляете примерные сроки и начинаете писать первые тесты $ тут и проходит первая ошибка - автотесты невозможно спрогнозировать что делать в первую очередь?
  12. Page Object 23 class MainActivityScreen: Screen<MainActivityScreen>() { val toFirstFeatureButton =

    KButton { withId(R.id.toFirstFeature)} val toSecondFeatureButton = KButton { withId(R.id.toSecondFeature)} val toThirdFeatureButton = KButton { withId(R.id.toThirdFeature)} }
  13. Kakao 24 private val mainScreen = MainActivityScreen() @Test fun testFirstFeature()

    { mainScreen { toFirstFeatureButton { isVisible() click() } } }
  14. Kakao 25 private val mainScreen = MainActivityScreen() @Test fun testFirstFeature()

    { mainScreen { toFirstFeatureButton { isVisible() click() } } } @Test fun testFirstFeature() { onView(withId(R.id.toFirstFeature)) .check(ViewAssertions.matches( ViewMatchers.withEffectiveVisibility( ViewMatchers.Visibility.VISIBLE))) onView(withId(R.id.toFirstFeature)).perform(click()) } Espresso
  15. Barista 26 https://github.com/SchibstedSpain/Barista Расширяет функционал espresso, но не решает проблемы

    громоздкого кода // Clear all app's SharedPreferences @Rule public ClearPreferencesRule clearPreferencesRule = new ClearPreferencesRule(); // Delete all tables from all the app's SQLite Databases @Rule public ClearDatabaseRule clearDatabaseRule = new ClearDatabaseRule(); // Delete all files in getFilesDir() and getCacheDir() @Rule public ClearFilesRule clearFilesRule = new ClearFilesRule(); // Use @AllowFlaky to let flaky tests pass if they pass any time. @Test @AllowFlaky(attempts = 5) public void some_flaky_test() throws Exception { // ... } • Scrolls on all views: Barista scrolls on all scrollable views, including NestedScrollView. Espresso only handles ScrollView and HorizontalScrollView
  16. AndroidJUnitRunner 29 • Общий раннер, встроенный в систему Android •

    Замена предыдущему раннеру InstrumentationTestRunner (deprecated) • Запускается путем adb комманд, например: 
 Running all tests: adb shell am instrument -w com.android.foo/ android.support.test.runner.AndroidJUnitRunner Running all tests in a class: adb shell am instrument -w -e class com.android.foo.FooTest com.android.foo/android.support.test.runner.AndroidJUnitRunner Running a single test: adb shell am instrument -w -e class com.android.foo.FooTest#testFoo com.android.foo/android.support.test.runner.AndroidJUnitRunner Running all tests in multiple classes: adb shell am instrument -w -e class com.android.foo.FooTest,com.android.foo.TooTest com.android.foo/ android.support.test.runner.AndroidJUnitRunner https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner https://developer.android.com/reference/android/test/InstrumentationTestRunner.html
  17. Почему использовать чистый раннер сложно 30 • Полное отсутствие конфигурации

    (за исключением только того, что можно указать какие конкретно тесты должны быть запущены) • Запускает только 1 инстанс приложения на все тесты
 При падении хотя бы одного теста – будет краш всех следующих тестов • Сложная работа с распараллеливанием тестов на несколько устройств
 
 
 Вывод: • Нужно искать какую-то обертку над этим раннером, в которой все эти проблемы решены
  18. Какие есть решения 31 В порядке технической сложности и реализованных

    фичей: • Orchestrator • connectedAndroidTest – нативная таска gradle • Spoon/composer • Marathon
  19. Orchestrator 32 По сути не является отдельным решением, помогает только

    «расширить» функционал стандартного раннера, путем создания отдельных инстансов на каждый тест + возможность подчищать состояние перед каждым тестом. +: • Создание инстансов на каждый тест (если 1 тест упадет, тестирование не остановится) • Возможность полностью очищать состояние приложения перед каждым тестом (pm clear) 
 - : • Требует установки дополнительных тестовых сервисов на каждое устройство • Нет отчета https://developer.android.com/training/testing/junit-runner
  20. connectedAndroidTest 33 ./gradlew connectedAndroidTest Поддерживает orchestrator с помощью дополнительных параметров

    Что делает: • Собирает проект и тестовую апк • Запускает тесты • Генерирует отчет Что плохо: • Практически никакой настройки нет (по сути можно только передавать параметры) • Нельзя отделить сборку проекта от запуска тестов -> следовательно тесты будут гнаться на том же компьютере, который собирает проект. Возможности запустить отдельно тесты без сборки проекта – нельзя.
  21. Spoon/composer 35 Spoon - полноценное решение для запуска тестов. В

    силу обстоятельств Spoon был заброшен, в то время как создатели Composer сделали свое решение практически используя те же наработки что и создатели Spoon.
 +: • Поддержка распараллеливания тестов. • Для запуска тестов нужно указать путь к собранному apk и apk с тестами. • Поддержка Orchestrator -: • Не умеют работать с flaky тестами http://square.github.io/spoon/ https://github.com/gojuno/composer
  22. Marathon 38 Еще более продвинутое решение в запуске тестов, из

    основных плюсов: +: • Кроссплатформа (iOS + Android) • Отдельный файл конфигурации • Свой встроенный аналог orchestrator (больше не нужно устанавливать на устройства тестовые сервисы) • Обработка flaky тестов из коробки • Умное распараллеливание (выбираем как тесты должны запускаться – например, каждый тест 5 раз или на всех подключенных устройствах) • Разбиение по группам версии ОС, архитектуры процессора и т.д. -: • Чуть более высокий порог входа • Версия 0.4.1, до релиза пока далеко (однако во время использования критичных багов замечено не было) https://github.com/Malinskiy/marathon
  23. Marathon отчет 39 За основу отчета взят отчет от Composer,

    однако чуть изменен вид самого теста. Теперь туда вставляется видео (или GIF в зависимости от версии OS) в случаях, если тест провалился. Также есть отчет о распараллеливании:
  24. Marathon где узнать больше? 41 Официальная документация: https://malinskiy.github.io/marathon/ Статьи о

    работе Marathon: https://proandroiddev.com/marathon-chapter-1-97f295054cc4 https://proandroiddev.com/marathon-chapter-2-1cde95cfdb87
  25. Что-то еще? 42 • Firebase Test Lab • Nitrogen https://www.youtube.com/watch?v=wYMIadv9iF8

    https://proandroiddev.com/robolectric-testing-with-androidjunitrunner-86292bceef25
  26. Что было дальше? 43 Когда появились первые результаты, столкнулись с

    тем, что: Espresso флекает Логирование действий автотестов Скриншоты при ошибках/ассертах А также ряд других серьезных вещей
  27. Пример 45 https://github.com/v1sar/UiTestApp 1) 3 фичи • 2 завязаны на

    1 компонент • 1 с прохождением FRW и инициализацией 2) Тесты на Kakao • Использован PageObject 3) Решены основные проблемы • Сброс состояния • Подмена классов • Асинхронная операция
  28. Как побороть flaky тест 47 fun KButton.clickWithWait() {
 var bool

    = true
 while (bool) {
 try {
 click()
 bool = false
 } catch (e: Exception) {
 idle(200)
 }
 }
 }
  29. Как побороть flaky тест 48 fun <T> attempt(action: () ->

    T, maxAttempt: Int) { var attempt = 0 while (attempt < maxAttempt) { try { action.invoke() break } catch (e: Throwable) { if (attempt == maxAttempt - 1) throw e attempt++ Thread.sleep(200) } } }
  30. Как побороть flaky тест 49 MainScreen { scanButton { isVisible()

    click() }d }d fun BaseActions.safeClick() {d attempt {dclick() }d }d fun BaseAssertions.safeIsVisible() {d attempt {disVisible() }d }d
  31. Как побороть flaky тест 50 MainScreen { scanButton { isVisible()

    click() }d }d fun BaseActions.safeClick() {d attempt {dclick() }d }d fun BaseAssertions.safeIsVisible() {d attempt {disVisible() }d }d MainScreen { scanButton { safeIsVisible() safeClick() }d }d
  32. !51 MainScreen { scanButton { safeIsVisible() safeClick() }а }а Логирование

    действий/скриншот fun BaseActions.safeClick() {d attempt {dclick() }d }d fun BaseAssertions.safeIsVisible() {d attempt {disVisible() }d }d
  33. !52 MainScreen { scanButton { safeIsVisible() safeClick() }а }а fun

    BaseActions.safeClick() { attempt { click() } logger.i("Some info") }a fun BaseAssertions.safeIsVisible() { try {а attempt { isVisible() } logger.i("Some info")а }аfinally {а screenshots.makeIfPossible("Some tag") }а }аа Логирование действий/скриншот
  34. Очистка состояния приложения 53 3 теста, просто запускают каждую активити

    фичи 13:53:33.729 W/UiTestApplication: UiTestApplication onCreate() 13:53:33.835 W/MainActivity: MainActivity onCreate() 13:53:34.016 W/TestButtonsMainActivity: testFirstFeature 13:53:34.110 W/FirstFeature: FirstFeature onCreate() 13:53:37.103 W/MainActivity: MainActivity onCreate() 13:53:37.205 W/TestButtonsMainActivity: testSecondFeature 13:53:37.291 W/SecondFeature: SecondFeature onCreate() 13:53:38.464 W/MainActivity: MainActivity onCreate() 13:53:38.672 W/TestButtonsMainActivity: testThirdFeature 13:53:38.867 W/ThirdFeature: ThirdFeature onCreate() 1 2 3 ~5 секунд
  35. Pm clear 54 1 2 3 14:04:40.508 W/UiTestApplication: UiTestApplication onCreate()

    14:04:40.685 W/MainActivity: MainActivity onCreate() 14:04:40.925 W/TestButtonsMainActivity: testFirstFeature 14:04:41.031 W/FirstFeature: FirstFeature onCreate() 14:04:43.728 W/UiTestApplication: UiTestApplication onCreate() 14:04:43.959 W/MainActivity: MainActivity onCreate() 14:04:44.137 W/TestButtonsMainActivity: testSecondFeature 14:04:44.302 W/SecondFeature: SecondFeature onCreate() 14:04:48.345 W/UiTestApplication: UiTestApplication onCreate() 14:04:48.507 W/MainActivity: MainActivity onCreate() 14:04:48.739 W/TestButtonsMainActivity: testThirdFeature 14:04:48.918 W/ThirdFeature: ThirdFeature onCreate() ~8 секунд
  36. !58 e2e Integrati on @Test
 fun testThirdFeature() {
 mMainScreen {


    toThirdFeatureButton {
 click()
 }
 }
 mThirdFeatureFrwScreen {
 proceedButton {
 click()
 }
 }
 mThirdFeatureScreen {
 featureDisclaimer {
 isVisible()
 }
 }
 } @Test
 fun testThirdFeatureFake() {
 rule.launchActivity(null)
 mMainActivityScreen {
 toThirdFeatureButton {
 click()
 }
 }
 mThirdFeatureFrwScreen {
 proceedButton {
 click()
 }
 }
 mThirdFeatureScreen {
 featureDisclaimer {
 isVisible()
 }
 }
 }
  37. 64 interface BaseAssertions { val view: ViewInteraction var root: Matcher<Root>

    /** * Checks if the view is displayed */ fun isDisplayed() { view.check( ViewAssertions.matches( ViewMatchers.isDisplayed() ) ) } //... } ViewInteraction
  38. 65 ViewInteractionDelegate interface BaseAssertions { val view: ViewInteractionDelegate var root:

    Matcher<Root> /** * Checks if the view is displayed */ fun isDisplayed() { view.check( ViewAssertions.matches( ViewMatchers.isDisplayed() ) ) } //... }
  39. 66 ViewInteractionDelegate interface ViewInteractionDelegate { fun perform(viewAction: ViewAction): ViewInteractionDelegate fun

    check(viewAssertion: ViewAssertion): ViewInteractionDelegate fun check(function: (View, NoMatchingViewException?) -> Unit): ViewInteractionDelegate fun withFailureHandler(function: (Throwable, Matcher<View>) -> Unit): ViewInteractionDelegate fun inRoot(rootMatcher: Matcher<Root>): ViewInteractionDelegate }
  40. 69 Kaspersky Lab | The Power of Protection Kakao (fork)

    DataInteraction Delegate WebInteraction Delegate KakaoConfigurator ViewInteraction Delegate DataInteraction DelegateImpl WebInteraction DelegateImpl ViewInteraction DelegateImpl Kaspresso
  41. Kakao 73 class MyTestCase { @Test fun someTest() { MainScreen

    { scanButton { isVisible() click() } } //... } }
  42. Kakao + Kaspresso 74 class MyTestCase : TestCase() { @Test

    fun someTest() { MainScreen { scanButton { isVisible() click() } } //... } }
  43. Permissions 77 /** * An interface to work with permissions.

    */ interface Permissions { /** * Passes the permission-requesting permissions dialog and allows permissions. */ fun allowViaDialog() /** * Passes the permission-requesting permissions dialog and denies permissions. */ fun denyViaDialog() }
  44. Internet 78 /** * An interface to work with internet

    settings. */ interface Internet { /** * Enables wi-fi and mobile data using adb. */ fun enable() /** * Disables wi-fi and mobile data using adb. */ fun disable() /** * Toggles only wi-fi. Note: it works only if flight mode is off. */ fun toggleWiFi(enable: Boolean) }
  45. Files 79 /** * An interface to work with file

    permissions. */ interface Files { /** * Performs adb push. * * @param serverPath a file path relative to the server directory. * @param devicePath a path to copy. */ fun push(serverPath: String, devicePath: String) }
  46. Зачем нам adb во время тестов? 81 Стандартные команды: •

    установка других apk во время теста • push / pull • выставление системных настроек
  47. Зачем нам adb во время тестов? 82 Нестандартные команды adb

    emu (работают только с эмуляторами): • установка геопозиции • выставление скорости сети, задержки • симулировать настоящих звонок • выставить значения акселерометра • имитация работы с отпечатком пальца • и т.д. https://developer.android.com/studio/run/emulator-console
  48. 86 https://developer.android.com/studio/run/emulator-networking Network Address Description 10.0.2.1 Router/gateway address 10.0.2.2 Special

    alias to your host loopback interface (i.e., 127.0.0.1 on your development machine) 10.0.2.3 First DNS server 10.0.2.4 / 10.0.2.5 / 10.0.2.6 Optional second, third and fourth DNS server (if any) 10.0.2.15 The emulated device network/ethernet interface 127.0.0.1 The emulated device loopback interface
  49. Server 88 Ваше приложение Test.apk Тесты GET 10.0.2.2:8080/?cmd="adb …" adb

    … @app.route('/') def execute_cmd(): cmd = request.args.get('cmd') p = os.popen(cmd).read() Flask
  50. $ adb shell generic_x86:/ # ifconfig radio0 Link encap:Ethernet HWaddr

    7a:7f:dd:f9:55:3r inet addr:192.168.200.2 Bcast:192.168.200.255 Mask:255.255.255.0 inet6 addr: fec0::4072:df55:c06e:32bc/64 Scope: Site inet6 addr: fe80::787f:bcff:fef0:444e/64 Scope: Link inet6 addr: fec0::787f:bcff:fef0:444e/64 Scope: Site UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:1191 errors:0 dropped:0 overruns:0 frame:0 TX packets:1314 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:436242 TX bytes:127317 lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope: Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:38 errors:0 dropped:0 overruns:0 frame:0 TX packets:38 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1 RX bytes:4542 TX bytes:4542 wlan0 Link encap:Ethernet HWaddr 01:22:33:44:55:66 Driver mac80211_hwsim inet addr:192.168.232.2 Bcast:192.168.239.255 Mask:255.255.248.0 inet6 addr: fe80::ff:fe44:5566/64 Scope: Link inet6 addr: fec0::61d3:bc35:c84d:1be3/64 Scope: Site inet6 addr: fec0::ff:fe44:5566/64 Scope: Site UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:50545 errors:0 dropped:0 overruns:0 frame:0 TX packets:34184 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:52675660 TX bytes:6458247 91
  51. $ adb shell generic_x86:/ # ifconfig lo Link encap:Local Loopback

    inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope: Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:46 errors:0 dropped:0 overruns:0 frame:0 TX packets:46 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1 RX bytes:5615 TX bytes:5615 $ adb shell generic_x86:/ # ifconfig radio0 Link encap:Ethernet HWaddr 7a:7f:dd:f9:55:3r inet addr:192.168.200.2 Bcast:192.168.200.255 Mask:255.255.255.0 inet6 addr: fec0::4072:df55:c06e:32bc/64 Scope: Site inet6 addr: fe80::787f:bcff:fef0:444e/64 Scope: Link inet6 addr: fec0::787f:bcff:fef0:444e/64 Scope: Site UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:1191 errors:0 dropped:0 overruns:0 frame:0 TX packets:1314 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:436242 TX bytes:127317 lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope: Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:38 errors:0 dropped:0 overruns:0 frame:0 TX packets:38 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1 RX bytes:4542 TX bytes:4542 wlan0 Link encap:Ethernet HWaddr 01:22:33:44:55:66 Driver mac80211_hwsim inet addr:192.168.232.2 Bcast:192.168.239.255 Mask:255.255.248.0 inet6 addr: fe80::ff:fe44:5566/64 Scope: Link inet6 addr: fec0::61d3:bc35:c84d:1be3/64 Scope: Site inet6 addr: fec0::ff:fe44:5566/64 Scope: Site UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:50545 errors:0 dropped:0 overruns:0 frame:0 TX packets:34184 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:52675660 TX bytes:6458247 92
  52. Port forwarding adb forward tcp:6100 tcp:7100 https://developer.android.com/studio/command-line/adb Server :7100 GET

    127.0.0.1:6100 adb reverse tcp:6100 tcp:7100 * API 21+ GET 127.0.0.1:6100 Server :7100 94
  53. adb forward <…> Ваше приложение Test.apk Тест Server Client Ваше

    приложение Test.apk Тест Server Ваше приложение Test.apk Тест Server adb forward <…> adb forward <…> 96
  54. Client 1. Установка соединения 2. AdbServer.makeAdbRequest("push test.apk") Test.apk Server 0.

    adb forward <…> object AdbServer 3. Execute "adb push test.apk" 4. Command complete 97
  55. Заключение 100 Где почерпнуть еще информации? 1) Митап по UI-тестированию

    в Авито 11.08.2018 https://www.youtube.com/watch?v=wZniIAhuLTE 2) PageObject + UiAutomator https://habr.com/post/416397/ 3) Kakao статья на хабре https://habr.com/post/339664/ 4) Android Dev Podcast #60 https://androiddev.apptractor.ru/android-dev-podkast-60/ 5) Первые попытки решить большинство проблем автотестов https://github.com/v1sar/UiTestApp 6) Выступление меня и Жени на Mobius 2019 SPb