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

При написании автотестов для таких приложений вы можете столкнуться с некоторыми трудностями. Например, автотест попытается выполнить определенное действие, но вместо ожидаемого результата на экране отобразится запрос разрешения, которое будет проигнорировано, и тест завершится неудачно. Или вы можете добиться такого поведения, когда на момент запуска тестов все разрешения будут даны, но при этом вы не сможете проверить, как ведет себя приложение, если пользователь отклонил запрос.



В этой статье мы покажем, как данные проблемы решает библиотека Kaspresso — популярный (1,7 тысячи звезд на Github) open-source-фреймворк для автотестов.

Мы подготовили пример и опубликовали его в нашем репозитории на Github. Скачиваем этот проект и запускаем приложение tutorial на одной из последних версий Android (API 23 и выше). После запуска приложения нажмите кнопку Make Call Activity.



У вас откроется экран, на котором есть два элемента — поле ввода и кнопка. В поле ввода можно указать какой-то номер телефона и кликнуть на кнопку Make Call для осуществления вызова.



Совершение звонков — одна из функций, для работы которой требуется запросить разрешение у пользователя. Поэтому у вас отобразится диалог с предложением позволить приложению управлять звонками, на котором есть кнопки «Разрешить» и «Отклонить».



Если мы нажмем Allow, то начнется вызов абонента по тому номеру, который вы указали в поле ввода. Обращаем ваше внимание, что если вы запускаете тест на реальном устройстве, то будет осуществлен настоящий телефонный звонок, за который могут быть списаны средства со счета.



При следующем открытии приложения разрешение больше не будет запрашиваться, оно сохраняется на устройстве. Если вы хотите отозвать разрешение, то можно это сделать в настройках. Для этого перейдите в раздел приложения, найдите нужное вам и заходите в раздел Permissions.



Здесь вы сможете зайти в любое разрешение и изменить значение с Allow на Deny или наоборот.

Второй способ, как это можно сделать, — при помощи команды adb shell:

adb shell pm revoke package_name permission_name

Для нашего приложения команда будет выглядеть так:

adb shell pm revoke com.kaspersky.kaspresso.tutorial android.permission.CALL_PHONE

После выполнения команды приложение снова запросит разрешение при следующей попытке совершить звонок.

Создаем тест


Тестировать приложение мы будем при помощи Kaspresso, поэтому первым делом необходимо подключить эту библиотеку к нашему проекту. В файл build.gradle добавляем следующие зависимости:

dependencies { 
androidTestImplementation("com.kaspersky.android-components:kaspresso:1.5.3") 
androidTestUtil("androidx.test:orchestrator:1.4.2") 
}

Подробно о том, как подключить Kaspresso к своему приложению, можно почитать здесь.

При написании тестов мы будем использовать Page Object. Если вы не знаете, что это, то рекомендуем ознакомиться с официальной документацией и с предыдущей нашей статьей.

Если коротко, то Page Object — это такой класс, который содержит ссылки на все view-элементы, с которыми нам нужно взаимодействовать.

Создаем Page Object экрана с кнопкой Make Call.

package com.kaspersky.kaspresso.tutorial.screen

import com.kaspersky.kaspresso.screens.KScreen
import com.kaspersky.kaspresso.tutorial.R
import io.github.kakaocup.kakao.edit.KEditText
import io.github.kakaocup.kakao.text.KButton

object MakeCallActivityScreen : KScreen<MakeCallActivityScreen>() {

    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null

    val inputNumber = KEditText { withId(R.id.input_number) }
    val makeCallButton = KButton { withId(R.id.make_call_btn) }
}

Чтобы попасть на этот экран, нужно будет в MainActivity кликнуть по соответствующей кнопке, добавляем эту кнопку в MainScreen.

package com.kaspersky.kaspresso.tutorial.screen

import com.kaspersky.kaspresso.screens.KScreen
import com.kaspersky.kaspresso.tutorial.R
import io.github.kakaocup.kakao.text.KButton

object MainScreen : KScreen<MainScreen>() {

    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null

    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
    val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
}

Можем создавать тест. Давайте пока просто откроем экран совершения звонка, введем какой-то номер и кликнем по кнопке.

package com.kaspersky.kaspresso.tutorial

import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Rule
import org.junit.Test

class MakeCallActivityTest : TestCase() {

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun checkSuccessCall() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText("111")
                makeCallButton.click()
            }
        }
    }
}

Запускаем тест. Тест пройден успешно.

В зависимости от того дали вы разрешение или нет, у вас может отобразиться диалог с запросом разрешения на совершение звонков.

Если вам незнаком использованный выше метод step или вы не знаете, почему его стоит использовать в тестах, то советуем почитать урок из нашего tutorial.

На данном этапе мы проверили работу нашего экрана, убедились, что есть возможность ввести номер и кликнуть на кнопку, но никак не проверили, происходит вызов по введенному номеру или нет. Для того чтобы проверить, происходит ли в данный момент вызов, можно использовать AudioManager, делается это следующим образом:

val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)

Можем добавить эту проверку отдельным шагом:

package com.kaspersky.kaspresso.tutorial

import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityTest : TestCase() {

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun checkSuccessCall() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText("111")
                makeCallButton.click()
            }
        }
        step("Check phone is calling") {
            val manager = device.context.getSystemService(AudioManager::class.java)
            Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
        }
    }
}

Перед запуском теста удалите приложение с устройства или отзовите разрешения при помощи команды adb shell. Также убедитесь, что вы запускаете тест на устройстве с API 23 и выше.

Запускаем тест. Тест провален.

Это произошло, потому что после клика по кнопке у пользователя было запрошено разрешение. Никто этого разрешения не дал, и следующий экран открыт не был.

Выдача разрешений при помощи TestRule


Есть несколько вариантов решения возникшей проблемы. Первый вариант — использовать GrantPermissionRule. Суть этого способа заключается в том, что мы создаем список разрешений, которые будут автоматически разрешены на тестируемом устройстве.

Для этого перед тестовым методом мы добавляем новое правило:

@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
    android.Manifest.permission.CALL_PHONE
)

В методе grant в круглых скобках мы через запятую перечисляем все требуемые разрешения, в данном случае оно всего одно, поэтому оставляем в таком виде. Тогда весь код теста будет выглядеть так:

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityTest : TestCase() {

    @get:Rule
    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
        android.Manifest.permission.CALL_PHONE
    )

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun checkSuccessCall() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText("111")
                makeCallButton.click()
            }
        }
        step("Check phone is calling") {
            val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
            Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
        }
    }
}

Перед запуском теста не забудьте отозвать все разрешения у приложения или удалить его с устройства.

Запускаем. В некоторых случаях этот тест пройдет успешно, а в некоторых — нет. Причину мы сейчас разберем.

FlakySafely для assertions


Мы стартуем звонок и следующим шагом проверяем, что телефон действительно звонит. Делаем это мы через метод Assert.assertTrue(…). Иногда устройство успевает осуществить набор номера до этой проверки, а иногда нет, поэтому в некоторых случаях тест завершается с ошибкой.

Kaspresso позволяет для любой проверки использовать механизм flakySafely. Подробнее о нем можно почитать в tutorial.

При его использовании одна и та же проверка запускается несколько раз в течение определенного таймаута до тех пор, пока не завершится успешно. По умолчанию этот тайм-аут равен 10 секундам, если в течение этого времени проверка так и не вернет true, то тест упадет. Для нас дефолтный тайм-аут подходит, оборачиваем вызов метода Assert.assertTrue в flakySafely.

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityTest : TestCase() {

    @get:Rule
    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
        android.Manifest.permission.CALL_PHONE
    )

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun checkSuccessCall() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText("111")
                makeCallButton.click()
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }
}

Сейчас тест работает, но в нем есть несколько проблем.

Во-первых, после окончания теста на устройстве все еще продолжается вызов абонента. Давайте добавим секции before и after и в секции, которая выполняется после теста, завершим вызов. Это можно сделать при помощи следующего кода: device.phone.cancelCall(«111»).

В этом методе мы обращаемся к экземпляру класса Device. Этот класс был добавлен в библиотеку Kaspresso, и он имеет множество возможностей: смена языка, смена ориентации устройства, работа с СМС и звонками и многое другое. Подробнее об этом классе вы можете почитать здесь.

Данный метод работает посредством adb-команд, поэтому перед запуском этого теста нужно убедиться, что у вас запущен adb-сервер, подробнее об этом вы можете почитать в нашем tutorial.

Теоретически вы могли бы сброс звонка вынести в отдельный step и запускать его последним шагом, не вынося в секцию after. Но это было бы плохим решением, поскольку в случае, если какой-то шаг завершится с ошибкой и тест будет провален, на устройстве будет продолжен вызов и никогда не сбросится. Преимущество секции after в том, что код внутри этого блока выполнится независимо от результата теста.

Чтобы не дублировать один и тот же номер в двух местах, давайте вынесем его в отдельную переменную, тогда код теста будет выглядеть так:

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityTest : TestCase() {

    @get:Rule
    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
        android.Manifest.permission.CALL_PHONE
    )

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    private val testNumber = "111"

    @Test
    fun checkSuccessCall() = before {
    }.after {
        device.phone.cancelCall(testNumber)
    }.run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }
}

Теперь после выполнения теста звонок завершается.

Вторая проблема — при использовании GrantPermissionRule мы можем проверить приложение только в состоянии, когда пользователь дал разрешение. При этом есть вероятность, что разработчики не предусмотрели вариант, когда запрос разрешения был отклонен, тогда результат может быть неожиданным, вплоть до того, что приложение будет крашиться. Необходимо проверять и такие сценарии, но использовать для этого GrantPermissionRule не получится, так как в этом случае разрешение всегда будет одобрено, и в тестах мы никогда не узнаем, какое будет поведение, если запрос отклонить.

Тестирование при помощи Device.Permissions


Один из вариантов решения проблемы — взаимодействовать с диалогом при помощи Kautomator, предварительно найдя все необходимые элементы интерфейса.

Kautomator — это компонент в Kaspresso, который позволяет взаимодействовать со сторонними приложениями и системными диалогами. Подробнее о нем можно почитать в нашем tutorial и в официальной документации.

В данной ситуации решение проблемы через Kautomator — не лучшее решение, в Kaspresso был добавлен намного более удобный способ — Device.Permissions. Он позволяет очень просто проверять диалоги разрешений, а также соглашаться с ними или отклонять.

Поэтому вместо Rule мы будем использовать объект Permissions, который можно получить у Device. Давайте сделаем это в отдельном классе, чтобы у вас сохранились оба варианта тестов. Класс, в котором мы сейчас работаем, переименуем в MakeCallActivityRuleTest.

И создаем новый класс MakeCallActivityDevicePermissionsTest. Код можно скопировать из текущего теста, за исключением GrantPermissionRule.

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityDevicePermissionsTest : TestCase() {
    
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    private val testNumber = "111"

    @Test
    fun checkSuccessCall() = before {
    }.after {
        device.phone.cancelCall(testNumber)
    }.run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }
}

Если мы запустим тест сейчас, то он завершится неудачно, так как мы не дали разрешений на совершение звонков. Давайте добавим еще один step, в котором дадим соответствующее разрешение через device.permissions. После указания объекта можно поставить точку и посмотреть, какие у него есть методы:



Есть возможность проверить, отображается ли диалог, а также отклонить или дать разрешение.

step("Accept permission") {
    Assert.assertTrue(device.permissions.isDialogVisible())
    device.permissions.allowViaDialog()
}

Таким образом мы убедимся, что диалог отображается, и дадим согласие на осуществление звонков.

Напоминаем, что диалог будет показан на версии Android API 23 и выше, как выполнять эти тесты на более ранних версиях, мы разберем в конце этой статьи.

Тут мы дважды написали device.permissions, давайте немного сократим код, применив функцию [apply]. А также проверку через assert давайте вынесем в метод flakySafely. Тогда весь код теста будет выглядеть так:

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityDevicePermissionsTest : TestCase() {

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    private val testNumber = "111"

    @Test
    fun checkSuccessCall() = before {
    }.after {
        device.phone.cancelCall(testNumber)
    }.run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Accept permission") {
            device.permissions.apply {
                flakySafely {
                    Assert.assertTrue(isDialogVisible())
                    allowViaDialog()
                }
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }
}

Запускаем. Тест пройден успешно.

Теперь мы можем с легкостью написать тест на то, что звонок не осуществляется, если разрешение дано не было. Для этого вместо allowViaDialog нужно указать denyViaDialog.

Также нужно изменить проверки в самом тесте, и не забудьте в новом методе удалить код из функции after, так как после отклонения разрешения звонок осуществлен не будет и после теста сбрасывать звонок больше не нужно.

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityDevicePermissionsTest : TestCase() {

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    private val testNumber = "111"

    @Test
    fun checkSuccessCall() = before {
    }.after {
        device.phone.cancelCall(testNumber)
    }.run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Accept permission") {
            device.permissions.apply {
                flakySafely {
                    Assert.assertTrue(isDialogVisible())
                    allowViaDialog()
                }
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }

    @Test
    fun checkCallIfPermissionDenied() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Deny permission") {
            device.permissions.apply {
                flakySafely {
                    Assert.assertTrue(isDialogVisible())
                    denyViaDialog()
                }
            }
        }
        step("Check stay on the same screen") {
            MakeCallActivityScreen {
                inputNumber.isDisplayed()
                makeCallButton.isDisplayed()
            }
        }
    }
}

Тестирование на разных версиях API


На современных версиях ОС Android (API 23 и выше) разрешения у пользователя запрашиваются во время работы приложения посредством диалога. Но в более ранних версиях они запрашивались в момент установки приложения, а во время работы считалось, что пользователь согласился со всеми требуемыми разрешениями.

Поэтому, если вы запускаете тест на устройствах с API ниже 23-й версии, то никакого запроса разрешений не будет, соответственно проверка диалога не требуется.

В тесте с использованием GrantPermissionRule никаких изменений не требуется, на старых версиях разрешение всегда есть, поэтому данная аннотация на работе теста никак не скажется. Но в тесте с использованием device.permissions изменения сделать необходимо, так как здесь мы явно проверяем работу диалога.

Вариантов здесь несколько. Во-первых, на таких устройствах нет смысла проверять работу приложения, если разрешение было отклонено, поэтому данный тест нужно просто пропускать. Для этого можно воспользоваться аннотацией @SdkSuppress. Тогда код метода checkCallIfPermissionDenied изменится на:

@SdkSuppress(minSdkVersion = 23)
@Test
fun checkCallIfPermissionDenied() = run {
    step("Open make call activity") {
        MainScreen {
            makeCallActivityButton {
                isVisible()
                isClickable()
                click()
            }
        }
    }
    step("Check UI elements") {
        MakeCallActivityScreen {
            inputNumber.isVisible()
            inputNumber.hasHint(R.string.phone_number_hint)
            makeCallButton.isVisible()
            makeCallButton.isClickable()
            makeCallButton.hasText(R.string.make_call_btn)
        }
    }
    step("Try to call number") {
        MakeCallActivityScreen {
            inputNumber.replaceText(testNumber)
            makeCallButton.click()
        }
    }
    step("Deny permission") {
        device.permissions.apply {
            flakySafely {
                Assert.assertTrue(isDialogVisible())
                denyViaDialog()
            }
        }
    }
    step("Check stay on the same screen") {
        MakeCallActivityScreen {
            inputNumber.isDisplayed()
            makeCallButton.isDisplayed()
        }
    }
}

Теперь данный тест будет выполняться только на новых версиях ОС Android, а на старых будет пропускаться.

Второй вариант решения проблемы — пропускать какие-то определенные шаги или заменять их другими в зависимости от уровня API. Например, в методе checkSuccessCall на старых девайсах мы можем пропустить шаг с проверкой диалога, для этого использовать такой код:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    step("Accept permission") {
        device.permissions.apply {
            flakySafely {
                Assert.assertTrue(isDialogVisible())
                allowViaDialog()
            }
        }
    }
}

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

Финальный код теста теперь будет выглядеть так:

package com.kaspersky.kaspresso.tutorial

import android.content.Context
import android.media.AudioManager
import android.os.Build
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.filters.SdkSuppress
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MakeCallActivityDevicePermissionsTest : TestCase() {

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    private val testNumber = "111"

    @Test
    fun checkSuccessCall() = before {
    }.after {
        device.phone.cancelCall(testNumber)
    }.run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            step("Accept permission") {
                device.permissions.apply {
                    flakySafely {
                        Assert.assertTrue(isDialogVisible())
                        allowViaDialog()
                    }
                }
            }
        }
        step("Check phone is calling") {
            flakySafely {
                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
            }
        }
    }

    @SdkSuppress(minSdkVersion = 23)
    @Test
    fun checkCallIfPermissionDenied() = run {
        step("Open make call activity") {
            MainScreen {
                makeCallActivityButton {
                    isVisible()
                    isClickable()
                    click()
                }
            }
        }
        step("Check UI elements") {
            MakeCallActivityScreen {
                inputNumber.isVisible()
                inputNumber.hasHint(R.string.phone_number_hint)
                makeCallButton.isVisible()
                makeCallButton.isClickable()
                makeCallButton.hasText(R.string.make_call_btn)
            }
        }
        step("Try to call number") {
            MakeCallActivityScreen {
                inputNumber.replaceText(testNumber)
                makeCallButton.click()
            }
        }
        step("Deny permission") {
            device.permissions.apply {
                flakySafely {
                    Assert.assertTrue(isDialogVisible())
                    denyViaDialog()
                }
            }
        }
        step("Check stay on the same screen") {
            MakeCallActivityScreen {
                inputNumber.isDisplayed()
                makeCallButton.isDisplayed()
            }
        }
    }
}

Итог


В этой статье мы рассмотрели два варианта работы с Permissions: GrantPermissionRule и device.permissions.

Также мы узнали, что второй вариант предпочтительнее по ряду причин:
  1. Объект Permissions дает возможность проверять отображение диалога с запросом разрешения.
  2. При использовании Permissions мы можем проверить поведение приложения не только при принятии разрешения, но также и при его отклонении.
  3. Тесты с применением GrantPermissionRule не будут работать, если разрешение было ранее отклонено. Потребуется переустановка приложения либо отмена выданных ранее разрешений через команду adb shell.
  4. Если во время выполнения теста отозвать разрешение при помощи команды adb shell, то в случае использования объекта Permissions тест будет работать корректно, а в случае использования GrantPermissionRule произойдет краш.

Если вам понравились возможности фреймворка и вы планируете использовать эту библиотеку в своих проектах, то присоединяйтесь к сообществу Kaspresso в Телеграме. Там мы постараемся оказать вам всю необходимую поддержку и проанонсируем следующие статьи из текущего цикла материалов о Kaspresso.

И не забудьте поставить звезду на Github ;)

Комментарии (2)


  1. Spinoza0
    05.04.2024 06:34

    Ценно, спасибо


  1. vrnvorona
    05.04.2024 06:34

    Intent моки вместо реального звонка?