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

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

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

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

Avatar for Dmitriy Movchan

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