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

Единственное, что вам нужно для UI-тестирования

MOSDROID
September 13, 2019

Единственное, что вам нужно для UI-тестирования

Егор Курников, Лаборатория Касперского at #MOSDROID 19 Potassium [in KasperskyLab HeadQuarter]

При написании UI-тестов для мобильных Android-приложений возникает множество различных проблем. Часть из них может быть решена с помощью ряда уже существующих инструментов, другая часть каждой компанией решается по-своему, эти решения довольно специфичны и не могут быть транслированы. Остальные проблемы остаются без решения.

Мы хотели создать универсальный фреймворк, который будет предоставлять готовый рецепт приготовления UI-тестов, а также обладать гибким API для расширения и кастомизации. Расскажу, как мы это делали, и что из этого получилось.

MOSDROID

September 13, 2019
Tweet

More Decks by MOSDROID

Other Decks in Programming

Transcript

  1. View Slide

  2. Зачем нам фреймворк?
    2

    View Slide

  3. Зачем нам фреймворк?
    ➔ Как начать писать автотесты?
    2

    View Slide

  4. ➔ Как начать писать автотесты?
    ➔ Какие инструменты выбрать?
    Зачем нам фреймворк?
    2

    View Slide

  5. UiAutomator
    Cappuccino
    Barista

    View Slide

  6. ➔ Как начать писать автотесты?
    ➔ Какие инструменты выбрать?
    ➔ Что делать если нужного инструмента нет?
    Зачем нам фреймворк?
    4

    View Slide

  7. ➔ Как начать писать автотесты?
    ➔ Какие инструменты выбрать?
    ➔ Что делать если нужного инструмента нет?
    Зачем нам фреймворк?
    5

    View Slide

  8. View Slide

  9. @RunWith(AndroidJUnit4::class)
    class TypeLoginTest {
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java)
    @Test
    fun test() {
    onView(withId(R.id.login_input_view))
    .perform(typeText(“MyLogin”))
    .check(matches(withText(“MyLogin”))
    }
    }
    7

    View Slide

  10. @RunWith(AndroidJUnit4::class)
    class TypeLoginTest {
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java)
    @Test
    fun test() {
    onView(withId(R.id.login_input_view))
    .perform(typeText(“MyLogin”))
    .check(matches(withText(“MyLogin”))
    }
    }
    7

    View Slide

  11. @RunWith(AndroidJUnit4::class)
    class TypeLoginTest {
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java)
    @Test
    fun test() {
    onView(withId(R.id.login_input_view))
    .perform(typeText(“MyLogin”))
    .check(matches(withText(“MyLogin”))
    }
    }
    7

    View Slide

  12. @RunWith(AndroidJUnit4::class)
    class TypeLoginTest {
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java)
    @Test
    fun test() {
    onView(withId(R.id.login_input_view))
    .perform(typeText(“MyLogin”))
    .check(matches(withText(“MyLogin”))
    }
    }
    7

    View Slide

  13. onView(ViewMatcher)
    ViewInteraction
    check(ViewAssertion)
    perform(ViewAction)
    8

    View Slide

  14. 9

    View Slide

  15. Чего мы хотим?
    10

    View Slide

  16. ➔ Хорошая читаемость
    Чего мы хотим?
    10

    View Slide

  17. @Test
    fun espressoTest() {
    onView(allOf(allOf(withId(R.id.espresso),
    isDescendantOfA(withId(R.id.coffee_variates))),
    isDescendantOfA(withId(R.id.content))))
    .check(matches(withEffectiveVisibility(View.VISIBLE)))
    }
    11

    View Slide

  18. @Test
    fun espressoTest() {
    onView(allOf(allOf(withId(R.id.espresso),
    isDescendantOfA(withId(R.id.coffee_variates))),
    isDescendantOfA(withId(R.id.content))))
    .check(matches(withEffectiveVisibility(View.VISIBLE)))
    }

    View Slide

  19. ➔ Хорошая читаемость
    ➔ Стабильность
    Чего мы хотим?
    12

    View Slide

  20. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    Чего мы хотим?
    12

    View Slide

  21. View Slide

  22. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    Чего мы хотим?
    14

    View Slide

  23. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    Чего мы хотим?
    14

    View Slide

  24. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    14

    View Slide

  25. 15

    View Slide

  26. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    16

    View Slide

  27. View Slide

  28. @Test
    fun espressoTest() {
    onView(allOf(allOf(withId(R.id.espresso),
    isDescendantOfA(withId(R.id.coffee_variates))),
    isDescendantOfA(withId(R.id.content))))
    .check(matches(withEffectiveVisibility(View.VISIBLE)))
    }
    18

    View Slide

  29. @Test
    fun kakaoTest() {
    mainScreen {
    myView.isVisible()
    }
    }
    18

    View Slide

  30. val button = KButton { withId(R.id.my_btn) }
    val textView = KTextView {
    withBackgroundColor(R.color.primary)
    isDescendantOfA {
    withId(R.id.content)
    withMatcher(MyCustomMatcher())
    }
    }
    KView
    19

    View Slide

  31. val button = KButton { ... }
    val textView = KTextView { ... }
    btn.click()
    textView {
    isVisible()
    hasText(R.string.title)
    }
    KView
    20

    View Slide

  32. open class KBaseView : BaseActions, BaseAssertions {
    override val view: ViewInteraction
    ...
    }
    21

    View Slide

  33. open class KBaseView : BaseActions, BaseAssertions {
    override val view: ViewInteraction
    ...
    }
    21

    View Slide

  34. open class KBaseView : BaseActions, BaseAssertions {
    override val view: ViewInteraction
    ...
    }
    21

    View Slide

  35. interface BaseActions {
    val view: ViewInteraction
    fun click() { ... }
    fun doubleClick() { ... }
    fun longClick() { ... }
    fun scrollTo() {
    view.perform(
    ViewActions.scrollTo()
    )
    }
    ...
    }
    interface BaseAssertions {
    val view: ViewInteraction
    fun isDisplayed() { ... }
    fun isVisible() { ... }
    fun hasBackgroundColor() { ... }
    fun isClickable() {
    view.check(
    ViewAssertions.matches(
    ViewMatchers.isClickable()
    )
    )
    }
    ...
    }
    22

    View Slide

  36. interface BaseActions {
    val view: ViewInteraction
    fun click() { ... }
    fun doubleClick() { ... }
    fun longClick() { ... }
    fun scrollTo() {
    view.perform(
    ViewActions.scrollTo()
    )
    }
    ...
    }
    interface BaseAssertions {
    val view: ViewInteraction
    fun isDisplayed() { ... }
    fun isVisible() { ... }
    fun hasBackgroundColor() { ... }
    fun isClickable() {
    view.check(
    ViewAssertions.matches(
    ViewMatchers.isClickable()
    )
    )
    }
    ...
    }
    22

    View Slide

  37. interface BaseActions {
    val view: ViewInteraction
    fun click() { ... }
    fun doubleClick() { ... }
    fun longClick() { ... }
    fun scrollTo() {
    view.perform(
    ViewActions.scrollTo()
    )
    }
    ...
    }
    interface BaseAssertions {
    val view: ViewInteraction
    fun isDisplayed() { ... }
    fun isVisible() { ... }
    fun hasBackgroundColor() { ... }
    fun isClickable() {
    view.check(
    ViewAssertions.matches(
    ViewMatchers.isClickable()
    )
    )
    }
    ...
    }
    22

    View Slide

  38. class FormScreen : Screen() {
    val login = KEditText {
    withId(R.id.login)
    }
    val password = KEditText {
    withId(R.id.password)
    }
    val submitBtn = KButton {
    withText(“Submit”)
    }
    val backBtn = KButton {
    withId(R.id.back)
    }
    }
    Submit
    login
    Sign in
    password
    Page object
    23

    View Slide

  39. @Test
    fun test() {
    MainScreen() {
    nextButton {
    isVisible()
    click()
    }
    }
    SecondScreen() {
    button1.isVisible()
    button2.click()
    }
    ThirdScreen() {
    editText {
    isVisible()
    hasText(R.string.my_text)
    }
    }
    } 24

    View Slide

  40. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    25

    View Slide

  41. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    25

    View Slide

  42. Interceptor
    Caller Called
    26

    View Slide

  43. onView(ViewMatcher)
    ViewInteraction
    check(ViewAssertion)
    perform(ViewAction)
    27

    View Slide

  44. onView(ViewMatcher)
    28
    interaction.perform(ViewAction)

    View Slide

  45. onView(ViewMatcher)
    interaction.perform(ViewAction)
    29
    Interceptor

    View Slide

  46. open class KBaseView : BaseActions, BaseAssertions {
    override val view: ViewInteraction
    ...
    }
    30

    View Slide

  47. open class KBaseView : BaseActions, BaseAssertions {
    override val view: ViewInteractionDelegate
    ...
    }
    30

    View Slide

  48. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  49. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  50. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  51. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  52. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  53. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  54. @Before
    fun setup() {
    Kakao.intercept {
    onViewInteraction {
    onCheck { interaction, assertion ->
    Log.d("KAKAO", "$interaction is checking $assertion")
    }
    onPerform { interaction, action ->
    Log.d("KAKAO", "$interaction is performing $action")
    }
    onAll { interaction ->
    Log.d("KAKAO", "$interaction")
    }
    }
    onDataInteraction { ... }
    onWebInteraction { ... }
    }
    }
    31

    View Slide

  55. mainScreen.intercept
    SecondScreen
    KView
    KView
    KView
    Kakao.intercept
    KView
    KView
    myView.intercept
    32

    View Slide

  56. onView(ViewMatcher)
    interaction.perform(ViewAction)
    33
    Interceptor

    View Slide

  57. Flaky safety
    fun flakySafely(action: () -> T): T {
    var cachedException: Throwable
    var startTime = System.currentTimeMillis()
    do {
    try {
    return action.invoke()
    } catch (e: Throwable) {
    Thread.sleep(intervalMs)
    cachedException = e
    }
    } while(System.currentTimeMillis() - startTime <= timeoutMs)
    throw cachedException
    }
    34

    View Slide

  58. onView(ViewMatcher)
    interaction.perform(ViewAction)
    35
    Interceptor

    View Slide

  59. onView(ViewMatcher)
    flakySafely { }
    Interceptor
    interaction.perform(ViewAction)
    36

    View Slide

  60. onView(ViewMatcher)
    flakySafely { }
    BehaviorInterceptor
    interaction.perform(ViewAction)
    36

    View Slide

  61. onView(ViewMatcher)
    FlakyBehaviorInterceptor
    interaction.perform(ViewAction)
    AutoScrollBehaviorInterceptor
    37

    View Slide

  62. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    38

    View Slide

  63. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    38

    View Slide

  64. BehaviorInterceptors
    39
    onView(ViewMatcher)
    check(ViewAssertion)
    perform(ViewAction)
    ViewInteraction

    View Slide

  65. 40
    interaction.perform(ViewAction)
    onView(ViewMatcher)
    viewAction.perform(UiContorller, View)
    BehaviorInterceptors

    View Slide

  66. 40
    interaction.perform(ViewAction)
    onView(ViewMatcher)
    viewAction.perform(UiContorller, View)
    BehaviorInterceptors

    View Slide

  67. Interceptor
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    41
    viewAction.perform(UiController, View)
    interaction.perform(ViewAction)
    BehaviorInterceptors

    View Slide

  68. WatcherInterceptor
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    41
    viewAction.perform(UiController, View)
    interaction.perform(ViewAction)
    BehaviorInterceptors

    View Slide

  69. WatcherInterceptor
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    41
    viewAction.perform(UiController, View)
    interaction.perform(ViewAction)
    BehaviorInterceptors

    View Slide

  70. WatcherInterceptor
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    42
    viewAction.perform(UiController, View)
    BehaviorInterceptors
    interaction.perform(ViewActionProxy)

    View Slide

  71. class ViewActionProxy(
    private val viewAction: ViewAction
    ): ViewAction {
    fun perform(ui: UiController, view: View) {
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    viewAction.perform(ui, view)
    }
    }
    interaction.perform(ViewActionProxy(viewAction))
    43
    BehaviorInterceptors

    View Slide

  72. class ViewActionProxy(
    private val viewAction: ViewAction
    ): ViewAction {
    fun perform(ui: UiController, view: View) {
    Log.d(TAG, “${viewAction.descr} on ${view.descr}”)
    viewAction.perform(ui, view)
    }
    }
    interaction.perform(ViewActionProxy(viewAction))
    43
    BehaviorInterceptors

    View Slide

  73. class ViewActionProxy(
    private val viewAction: ViewAction
    ): ViewAction {
    fun perform(ui: UiController, view: View) {
    watcherInterceptors.forEach { it.intercept(ui, view) }
    viewAction.perform(ui, view)
    }
    }
    interaction.perform(ViewActionProxy(viewAction))
    44
    BehaviorInterceptors

    View Slide

  74. ViewActionProxy
    WatcherInterceptors
    viewAction.perform(UiController, View)
    interaction.perform(ViewActionProxy)
    45
    BehaviorInterceptors

    View Slide

  75. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    46

    View Slide

  76. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    46

    View Slide

  77. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    46

    View Slide

  78. ➔ Accessibility
    ➔ Activities
    ➔ Apps
    ➔ Exploit
    ➔ Files
    ➔ Keyboard
    ➔ Location
    ➔ Network
    ➔ Permissions
    ➔ Phone
    ➔ Screenshots
    Device
    47

    View Slide

  79. ➔ Accessibility
    ➔ Activities
    ➔ Apps
    ➔ Exploit
    ➔ Files
    ➔ Keyboard
    ➔ Location
    ➔ Network
    ➔ Permissions
    ➔ Phone
    ➔ Screenshots
    interface Apps {
    fun install(apkPath: String)
    fun uninstall(pckgName: String)
    fun launch(pckgName: String, data: Uri?)
    fun openRecent(contentDescr: String)
    fun kill(pckgName: String)
    }
    Device
    48

    View Slide

  80. ➔ Accessibility
    ➔ Activities
    ➔ Apps
    ➔ Exploit
    ➔ Files
    ➔ Keyboard
    ➔ Location
    ➔ Network
    ➔ Permissions
    ➔ Phone
    ➔ Screenshots
    Device
    interface Files {
    fun push(
    serverPath: String,
    devicePath: String
    )
    fun remove(path: String)
    }
    49

    View Slide

  81. ➔ Accessibility
    ➔ Activities
    ➔ Apps
    ➔ Exploit
    ➔ Files
    ➔ Keyboard
    ➔ Location
    ➔ Network
    ➔ Permissions
    ➔ Phone
    ➔ Screenshots
    Device
    interface Network {
    fun enable()
    fun disable()
    fun enableWiFi()
    fun disableWiFi()
    }
    50

    View Slide

  82. Что умеет ADB?
    ➔ Установка apk
    ➔ push/pull
    ➔ Выставление системных настроек
    51

    View Slide

  83. Что умеет ADB?
    ➔ Установка apk
    ➔ push/pull
    ➔ Выставление системных настроек
    adb emu
    ➔ Установка геопозиции
    ➔ Выставление скорости сети, задержки
    ➔ Симуляция вызовов
    ➔ Выставление значений акселерометра
    ➔ Имитация работы с отпечатком пальца
    51

    View Slide

  84. Client app
    App
    Test app
    Tests
    HOST
    52

    View Slide

  85. Virtual routing
    10.0.2.2 - Special alias to your loopback
    interface (i.e. 127.0.0.1 on your
    development machine)
    53

    View Slide

  86. Client app
    HOST
    App
    Server Test app
    Tests
    54

    View Slide

  87. Client app
    HOST
    App
    Server Test app
    Tests
    GET 10.0.2.2:8080/?cmd=”adb ...”
    adb ...
    54

    View Slide

  88. View Slide

  89. $ 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
    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 MULTICAST MTU:655536 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
    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
    RX bytes:52675660 TX bytes: 6458247
    56

    View Slide

  90. $ 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 MULTICAST MTU:655536 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
    RX bytes:4542 TX bytes: 4542
    57

    View Slide

  91. View Slide

  92. Port forwarding
    ➔ adb forward tcp:6100 tcp:8500
    HOST
    localhost:8500
    GET localhost:6100
    localhost:6100
    59

    View Slide

  93. Client app
    HOST
    App
    Test app
    Tests
    60
    Client
    localhost: 6100

    View Slide

  94. Client app
    App
    Test app
    Server
    localhost:8500
    Tests
    HOST
    Client
    localhost: 6100
    61

    View Slide

  95. Client app
    App
    adb forward
    tcp:6100 tcp:8500 Client
    localhost: 8500
    Test app
    Tests
    HOST
    62
    Client
    localhost: 6100
    Server
    localhost:8500

    View Slide

  96. Client app
    App
    Test app
    Tests
    HOST
    63
    Client
    localhost: 6100
    Client
    localhost: 8500
    Server
    localhost:8500

    View Slide

  97. adb forward ...
    Cli
    ent
    app
    Server
    App
    Cli
    ent
    app
    Server
    App
    Cli
    ent
    app
    Server
    App
    adb forward ...
    adb forward ...
    HOST
    64
    Client
    localhost: 6100

    View Slide

  98. class AdbServer {
    fun performCmd(
    vararg commands: String
    ): String
    fun performAdb(
    vararg commands: String
    ): String
    fun perfromShell(
    vararg commands: String
    ): String
    fun disconnectServer()
    }
    1. Connect
    2. performAdb("...")
    3. Execute "..."
    5. Disconnect
    65
    Client
    localhost: 8500
    Server
    localhost:8500
    4. Command complete

    View Slide

  99. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    66

    View Slide

  100. ➔ Хорошая читаемость
    ➔ Стабильность
    ➔ Логирование
    ➔ Скриншоты
    ➔ Работа с OS Android
    ➔ Архитектура
    Чего мы хотим?
    66

    View Slide

  101. Kaspresso

    View Slide

  102. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    68
    Kaspresso

    View Slide

  103. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @get:Rule
    val testCaseRule =
    TestCaseRule(javaClass.simpleName)
    @Test
    fun test() { ... }
    }
    69
    Kaspresso

    View Slide

  104. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase() {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    70
    Kaspresso

    View Slide

  105. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder().apply { ... }
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    71
    Kaspresso

    View Slide

  106. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder.default()
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    72
    Kaspresso

    View Slide

  107. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder.default().apply { ... }
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    73
    Kaspresso

    View Slide

  108. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder.default().apply {
    viewBehaviorInterceptors = mutableListOf()
    }
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    74
    Kaspresso

    View Slide

  109. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder.default().apply {
    viewBehaviorInterceptors.add(MyInterceptor())
    }
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    75
    Kaspresso

    View Slide

  110. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest : TestCase(
    Kaspresso.Builder.default().apply {
    viewBehaviorInterceptors.add(MyInterceptor())
    flakySafetyParams.timeoutMs = 1_000
    }
    ) {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    } 76
    Kaspresso

    View Slide

  111. abstract class TestCase(
    kaspressoBuilder: Kaspresso.Builder
    ) {
    internal val kaspresso: Kaspresso =
    kaspressoBuilder.build()
    ...
    }
    Kaspresso initialization
    77

    View Slide

  112. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    78

    View Slide

  113. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    78

    View Slide

  114. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    78

    View Slide

  115. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    78

    View Slide

  116. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    private val mainScreen = MainScreen()
    private val homeScreen = HomeScreen()
    @get:Rule
    val activityTestRule =
    ActivityTestRule(MainActivity::class.java, true, false)
    @Test
    fun test() { ... }
    }
    79

    View Slide

  117. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    MainScreen() {
    homeButton.click()
    }
    HomeScreen() {
    title {
    isVisible()
    hasAnyText()
    }
    }
    }
    }
    80

    View Slide

  118. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    //1st step
    MainScreen() {
    homeButton.click()
    }
    //2nd step
    HomeScreen() {
    title {
    isVisible()
    hasAnyText()
    }
    }
    }
    }
    81

    View Slide

  119. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    //1st step
    Log.i(“1 step started”)
    MainScreen() {
    homeButton.click()
    }
    Log.i(“1st step succeeded”)
    //2nd step
    Log.i(“2nd step started”)
    HomeScreen() {
    title {
    isVisible()
    hasAnyText()
    }
    }
    Log.i(“2nd step succeeded”)
    }
    } 82

    View Slide

  120. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    //1st step
    try {
    Log.i(“1st step started”)
    MainScreen() {
    homeButton.click()
    }
    Log.i(“1st step succeeded”)
    } catch(e: Throwable) {
    Log.i(“1st step failed”)
    takeScreenshot()
    throw e
    }
    //2nd step
    ...
    }
    }
    83

    View Slide

  121. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    step(“1. Open Home screen”) {
    MainScreen() {
    homeButton.click()
    }
    }
    step(“2. Check Home title”) {
    HomeScreen() {
    title {
    isVisible()
    hasAnyText()
    }
    }
    }
    }
    }
    84

    View Slide

  122. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    85

    View Slide

  123. action.invoke()
    throw exception
    next step
    step success step fail
    interceptBefore()
    interceptFinally()
    interceptFailure()
    interceptSuccess()
    step start
    start success fail
    ➔ Logging
    ➔ Screenshots
    ➔ ...
    StepInterceptors
    86

    View Slide

  124. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    87

    View Slide

  125. I/KASPRESSO: ___________________________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: ___________________________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_1;text=Button 1;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 301 millis.
    I/KASPRESSO: ___________________________________________________________________________
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check with string from resource id: <2131558461> on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 2 seconds and 138 millis.
    I/KASPRESSO: ___________________________________________________________________________
    88

    View Slide

  126. I/KASPRESSO: _____________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: single click on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: ______________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_1;text=Button 1;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 301 millis.
    I/KASPRESSO: ___________________________________________________________________________
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check with string from resource id: <2131558461> on AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 2 seconds and 138 millis.
    I/KASPRESSO: ___________________________________________________________________________
    89

    View Slide

  127. I/KASPRESSO: _____________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: single click on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    E/KASPRESSO: Failed to interact with view matching: (with id:
    actibity_main_button_back) because of AssertionFailedError
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest FAILED.
    It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: ______________________________________________________________
    90

    View Slide

  128. I/KASPRESSO: _____________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=activity_main_button_home;text=Home;)
    E/KASPRESSO: Failed to interact with view matching: (with id: actibity_main_button_back)
    because of AssertionFailedError
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest FAILED.
    It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: ______________________________________________________________
    91
    sdcard/screenshots
    OpenHomeScreenTest_step_1_failure
    Sign in

    View Slide

  129. @Test
    fun test() {
    step("Pass first run wizzard") {
    scenario(
    FirstRunWizzardScenario()
    )
    step("Substep #1") { ... }
    step("Substep #2") {
    step("Inner substep #3") { ... }
    }
    }
    }
    92

    View Slide

  130. @Test
    fun test() {
    step("Pass first run wizzard") {
    scenario(
    FirstRunWizzardScenario()
    )
    step("Substep #1") { ... }
    step("Substep #2") {
    step("Inner substep #3") { ... }
    }
    }
    }
    92

    View Slide

  131. @Test
    fun test() {
    step("Pass first run wizzard") {
    scenario(
    FirstRunWizzardScenario()
    )
    step("Substep #1") { ... }
    step("Substep #2") {
    step("Inner substep #3") { ... }
    }
    }
    }
    92

    View Slide

  132. @Test
    fun test() {
    step("Pass first run wizzard") {
    scenario(
    FirstRunWizzardScenario()
    )
    step("Substep #1") { ... }
    step("Substep #2") {
    step("Inner substep #3") { ... }
    }
    }
    }
    class FirstRunWizzardScenario : Scenario() {
    override val steps: TestContext.() -> Unit = {
    step("Check first wizzard screen") { ... }
    step("Check second wizzard screen") { ... }
    }
    }
    92

    View Slide

  133. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    93

    View Slide

  134. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    step("0. Turn off wifi") { ... }
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    94

    View Slide

  135. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    step("0. Turn off wifi") { ... }
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    step("8. Turn on wifi") { ... }
    }
    }
    95

    View Slide

  136. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before {
    turnOffWiFi()
    }.after {
    turnOnWiFi()
    }.run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    }
    96

    View Slide

  137. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before {
    device.network.toggleWiFi(enable = false)
    }.after {
    device.network.toggleWiFi(enable = true)
    }.run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    }
    97

    View Slide

  138. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    98

    View Slide

  139. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    device.files.push()
    device.phone.emulateCall(NUMBER)
    device.location.enableGps()
    device.exploit.pressBack()
    device.screenshots.take(TAG)
    99

    View Slide

  140. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    100

    View Slide

  141. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }.run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    step("4. Check some stuff is done") { ... }
    step("5. Do another stuff") { ... }
    step("6. Check another stuff is done") { ... }
    step("7. 42") { ... }
    }
    }
    }
    101

    View Slide

  142. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .init {
    company { name = "Microsoft"; city = "Redmond" }
    company { name = "Google"; city = "Mountain View" }
    owner { firstName = "Satya"; secondName = "Nadella" }
    owner { firstName = "Sundar"; secondName = "Pichai" }
    }
    .run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    ...
    }
    }
    }
    102

    View Slide

  143. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .init {
    company { name = "Microsoft"; city = "Redmond" }
    company { name = "Google"; city = "Mountain View" }
    owner { firstName = "Satya"; secondName = "Nadella" }
    owner { firstName = "Sundar"; secondName = "Pichai" }
    }.transform {
    bind(ownerSurname = "Nadella", companyName = "Microsoft")
    bind(ownerSurname = "Pichai", companyName = "Google")
    }
    .run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    ...
    }
    }
    } 103

    View Slide

  144. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .init {
    company { name = "Microsoft"; city = "Redmond" }
    company { name = "Google"; city = "Mountain View" }
    owner { firstName = "Satya"; secondName = "Nadella" }
    owner { firstName = "Sundar"; secondName = "Pichai" }
    }.transform {
    bind(ownerSurname = "Nadella", companyName = "Microsoft")
    bind(ownerSurname = "Pichai", companyName = "Google")
    }
    .run {
    step("1. Check description") {
    MainScreen().descriptionTextView {
    hasText(data.owners.first().firstName ?: "")
    }
    }
    }
    }
    } 104

    View Slide

  145. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    105

    View Slide

  146. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    105

    View Slide

  147. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .init { ... }.transform { ...}
    .run {
    step("1. Open Home screen") {
    testLogger.i("I am test logger")
    MainScreen() {
    homeButton.click()
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    }
    106

    View Slide

  148. I/KASPRESSO: _____________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    I/KASPRESSO_SPECIAL: I am test logger
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: single click on
    AppCompatButton(id=activity_main_button_home;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: ______________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_1;text=Button 1;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 0 seconds and 301 millis.
    I/KASPRESSO: ___________________________________________________________________________
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest
    I/KASPRESSO: single click on AppCompatButton(id=button_2;text=Button 2;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: Check with string from resource id: <2131558461> on AppCompatEditText(id=edit;text=Some text;)
    I/KASPRESSO: TEST STEP: "3. Do some stuff" in OpenHomeScreenTest SUCCEED.
    It took 0 minutes, 2 seconds and 138 millis.
    I/KASPRESSO: ___________________________________________________________________________
    107

    View Slide

  149. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    108

    View Slide

  150. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .run {
    step("1. Open Home screen") {
    MainScreen() {
    homeButton.click()
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    }
    109

    View Slide

  151. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .run {
    step("1. Open Home screen") {
    adbServer.performAdb(
    "adb push foo.txt /sdcard/foo.txt"
    )
    MainScreen() {
    homeButton.click()
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    }
    110

    View Slide

  152. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    111

    View Slide

  153. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .run {
    step("1. Open Home screen") {
    MainScreen() {
    flakySafely(timeoutMs = 7_000) {
    homeButton.click()
    }
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    }
    112

    View Slide

  154. ➔ device
    ➔ data
    ➔ testLogger
    ➔ adbServer
    ➔ flakySafely
    ➔ compose
    Test context
    113

    View Slide

  155. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .run {
    step("1. Open Home screen") {
    MainScreen() {
    compose {
    or(redBtn) { hasText("blue") }
    or(blueBtn) { hasText("blue") }
    }
    homeButton.click()
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    } 114

    View Slide

  156. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }.after { ... }
    .run {
    step("1. Open Home screen") {
    MainScreen() {
    redBtn.compose {
    or { hasText("blue") }
    or { hasText("red") }
    }
    homeButton.click()
    }
    }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    } 115

    View Slide

  157. @RunWith(AndroidJUnit4::class)
    class OpenHomeScreenTest: TestCase() {
    @Test
    fun test() {
    before { ... }
    .after { ... }
    .init { ... }
    .transform { ...}
    .run {
    step("1. Open Home screen") { ... }
    step("2. Check Home title") { ... }
    step("3. Do some stuff") { ... }
    }
    }
    }
    116

    View Slide

  158. step
    #2
    step #1
    step #3
    run
    before
    after
    step #2
    Test runner
    Test report
    success
    fail
    success
    fail
    success
    fail
    TestRunInterceptors
    ➔ Logging
    ➔ Screenshots
    ➔ ...
    start
    start
    start
    start success fail
    117

    View Slide

  159. data class Kaspresso(
    internal val libLogger: UiTestLogger,
    internal val testLogger: UiTestLogger,
    internal val device: Device,
    internal val adbServer: AdbServer,
    internal val flakySafetyParams: FlakySafetyParams,
    internal val autoScrollParams: AutoScrollParams,
    internal val viewActionWatcherInterceptors: List,
    internal val viewAssertionWatcherInterceptors: List,
    internal val atomWatcherInterceptors: List,
    internal val webAssertionWatcherInterceptors: List,
    internal val viewBehaviorInterceptors: List,
    internal val dataBehaviorInterceptors: List,
    internal val webBehaviorInterceptors: List,
    internal val stepInterceptors: List,
    internal val testRunInterceptors: List
    )
    Kaspresso configuration
    118

    View Slide

  160. I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home Screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest
    SUCCEED. It took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
    I/KASPRESSO: Check with text: is "111" on
    AppCompatEditText(id=edit;text=111;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    SUCCEED. It took 0 minutes, 0 seconds and 621 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST PASSED
    I/KASPRESSO: ----------------------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    119

    View Slide

  161. I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed
    with result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home Screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
    I/KASPRESSO: Check with text: is "111" on AppCompatEditText(id=edit;text=111;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 621 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST PASSED
    I/KASPRESSO: ----------------------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    120

    View Slide

  162. I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: TEST SECTION
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home Screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
    I/KASPRESSO: Check with text: is "111" on AppCompatEditText(id=edit;text=111;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 621 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST PASSED
    I/KASPRESSO: ----------------------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    121

    View Slide

  163. I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home Screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
    I/KASPRESSO: Check with text: is "111" on AppCompatEditText(id=edit;text=111;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 621 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed
    with result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: TEST PASSED
    I/KASPRESSO: ----------------------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    122

    View Slide

  164. I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "1. Open Home Screen" in OpenHomeScreenTest
    I/KASPRESSO: Check view has effective visibility=VISIBLE on
    AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: single click on AppCompatButton(id=home_btn;text=Home;)
    I/KASPRESSO: TEST STEP: "1. Open Home screen" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 618 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest
    I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
    I/KASPRESSO: Check with text: is "111" on AppCompatEditText(id=edit;text=111;)
    I/KASPRESSO: TEST STEP: "2. Check Home title" in OpenHomeScreenTest SUCCEED. It
    took 0 minutes, 0 seconds and 621 millis.
    I/KASPRESSO: __________________________________________________________
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: TEST PASSED
    I/KASPRESSO: ---------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    123

    View Slide

  165. I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed
    with result=CommandResult(status=FAILED)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION FAILED
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed with
    result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: TEST FAILED
    I/KASPRESSO: ----------------------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    124

    View Slide

  166. I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: BEFORE TEST SECTION
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: command=shell svc data disable was performed with
    result=CommandResult(status=FAILED)
    I/KASPRESSO: ----------------------------------------------------------
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: AFTER TEST SECTION
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: command=shell svc data enable was performed
    with result=CommandResult(status=SUCCESS)
    I/KASPRESSO: ---------------------------------------------
    I/KASPRESSO: TEST FAILED
    I/KASPRESSO: ---------------------------------------------
    Sign in
    screenshots saved at
    sdcard/screenshots
    125

    View Slide

  167. ➔ Читаемость Kakao
    Kaspresso
    126

    View Slide

  168. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    Kaspresso
    126

    View Slide

  169. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    Kaspresso
    126

    View Slide

  170. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    Kaspresso
    126

    View Slide

  171. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    ➔ Предоставляет интерфейс взаимодействия с OS Android
    Kaspresso
    126

    View Slide

  172. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    ➔ Предоставляет интерфейс взаимодействия с OS Android
    ➔ Умеет в ADB
    Kaspresso
    126

    View Slide

  173. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    ➔ Предоставляет интерфейс взаимодействия с OS Android
    ➔ Умеет в ADB
    ➔ Предоставляет решение по архитектуре
    Kaspresso
    126

    View Slide

  174. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    ➔ Предоставляет интерфейс взаимодействия с OS Android
    ➔ Умеет в ADB
    ➔ Предоставляет решение по архитектуре
    ➔ Гибко настраивается и конфигурируется
    Kaspresso
    126

    View Slide

  175. ➔ Читаемость Kakao
    ➔ Реализует flaky safety
    ➔ Предоставляет удобный механизм логирования
    ➔ Позволяет снимать скриншоты
    ➔ Предоставляет интерфейс взаимодействия с OS Android
    ➔ Умеет в ADB
    ➔ Предоставляет решение по архитектуре
    ➔ Гибко настраивается и конфигурируется
    ➔ Может быть кастомизирован
    Kaspresso
    126

    View Slide

  176. Our team
    Евгений
    Мацюк
    Андрей
    Антипов
    Егор
    Курников
    Дмитрий
    Мовчан
    Руслан
    Мингалиев
    Александр
    Блинов
    Алексей
    Творогов
    Николай
    Нестеров
    Павел
    Стрельченко
    Ринат
    Агишев
    127
    Дмитрий
    Воронин
    Сергей
    Занкин

    View Slide

  177. ➔ Kaspresso:
    https://github.com/KasperskyLab/Kaspresso
    ➔ AdbServer:
    https://github.com/KasperskyLab/AdbServer
    ➔ Чат поддержки:
    https://t.me/kaspresso
    Links
    128

    View Slide

  178. ВНИМАНИЕ!
    Спасибо за внимание

    View Slide