How to Test an Android App That Requires Permissions

Often, for an application to work correctly, it needs access to certain functions of a mobile device: camera, voice recording, making calls, sending SMS messages, etc. The application can access and use them only if the user gives permission. .

When writing automated tests for such applications, you may encounter some difficulties. For example, Autotest will try to perform a certain action, but instead of the expected result, the screen will display a permission prompt, which will be ignored and the test will fail. Or you can achieve a behavior where all permissions are granted when the tests are run, but you cannot test how the application behaves if the user rejects the request.

In this article we will show how these problems are solved Kaspresso library – a popular (1.7 thousand stars on Github) open-source framework for autotests.


We prepared an example and published it
in our Github repository. Download this project and run the tutorial application on one of the latest versions of Android (API 23 and higher). After launching the application, click the Make Call Activity button.

You will see a screen on which there are two elements – an input field and a button. You can enter a phone number in the input field and click on the Make Call button to make a call.

Making calls is one of the features that requires permission from the user. Therefore, you will be presented with a dialog asking you to allow the application to manage calls, which has buttons “Allow” and “Reject”.

If we click Allow, a call will begin to the subscriber at the number that you specified in the input field. Please note that if you run the test on a real device, an actual phone call will be made and your account may be charged.

The next time you open the app, permission will no longer be requested; it will be saved on your device. If you want to revoke permission, you can do so in the settings. To do this, go to the application section, find the one you need and go to the Permissions section.

Here you can go to any resolution and change the value from Allow to Deny or vice versa.

The second way this can be done is using the adb shell command:

adb shell pm revoke package_name permission_name

For our application the command will look like this:

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

After executing the command, the application will ask for permission again the next time you try to make a call.

Creating a test

We will test the application using Kaspresso, so the first thing we need to do is connect this library to our project. Add the following dependencies to the build.gradle file:

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

For more information on how to connect Kaspresso to your application, please see:

read here

.

When writing tests we will use Page Object. If you don't know what this is, we recommend read the official documentation and with our previous article.

In short, Page Object is a class that contains references to all view elements with which we need to interact.

Create a Screen Page Object with a Make Call button.

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) }
}

To get to this screen, you will need to click on the corresponding button in MainActivity, add this button to 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) }
}

We can create a test. For now, let's just open the call making screen, enter a number and click on the button.

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()
            }
        }
    }
}

Let's run the test. The test was completed successfully.

Depending on whether you have given permission or not, you may be presented with a dialog asking for permission to make calls.

If you are unfamiliar with the step method used above or you don’t know why it should be used in tests, we recommend reading lesson from our tutorial.

At this stage, we checked the operation of our screen, made sure that it was possible to enter a number and click on the button, but did not check in any way whether a call was made to the entered number or not. In order to check whether a call is currently in progress, you can use AudioManager, this is done as follows:

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

We can add this check as a separate step:

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)
        }
    }
}

Before running the test, remove the application from the device or revoke permissions using the adb shell command. Also make sure you are running the test on a device with API 23 or higher.

Let's run the test. Test failed.

This happened because the user was asked for permission after clicking the button. No one gave this permission, and the next screen was not opened.

Granting permissions using TestRule

There are several options for solving this problem. The first option is to use GrantPermissionRule. The essence of this method is that we create a list of permissions that will be automatically allowed on the device under test.

To do this, we add a new rule before the test method:

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

In the grant method, in parentheses, we list all the required permissions, separated by commas; in this case, there is only one, so we leave it as is. Then the entire test code will look like this:

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)
        }
    }
}

Before running the test, be sure to revoke all permissions from the app or remove it from your device.

Let's launch. In some cases this test will pass and in others it will not. We will now look at the reason.

FlakySafely for assertions

We start the call and the next step is to check that the phone is actually ringing. We do this through the Assert.assertTrue(…) method. Sometimes the device manages to dial the number before this test, and sometimes it does not, so in some cases the test fails with an error.

Kaspresso allows you to use the flakySafely mechanism for any verification. You can learn more about it read the tutorial.

When used, the same check is run multiple times within a specified timeout until it succeeds. By default, this timeout is 10 seconds; if during this time the test does not return true, the test will fail. For us, the default timeout is suitable, we wrap the call to the Assert.assertTrue method in 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)
            }
        }
    }
}

The test is working now, but there are a few problems.

Firstly, after the end of the test, the call to the subscriber is still ongoing on the device. Let's add before and after sections and in the section that runs after the test, complete the call. This can be done using the following code: device.phone.cancelCall(“111”).

In this method, we access an instance of the Device class. This class has been added to the Kaspresso library, and it has many capabilities: changing the language, changing the device orientation, working with SMS and calls, and much more. You can learn more about this class read here.

This method works using adb commands, so before running this test you need to make sure that you have an adb server running, you can read more about this read in our tutorial.

Theoretically, you could put the call reset in a separate step and run it as the last step, without putting it in the after section. But this would be a bad solution because if some step fails and the test fails, the device will continue to call and never reset. The advantage of the after section is that the code inside this block will be executed regardless of the test result.

In order not to duplicate the same number in two places, let's put it in a separate variable, then the test code will look like this:

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)
            }
        }
    }
}

Now after the test is completed the call ends.

The second problem is that when using GrantPermissionRule, we can only check the application in the state where the user has given permission. In this case, there is a possibility that the developers did not provide for the option when the permission request was rejected, then the result may be unexpected, even to the point that the application will crash. It is necessary to check such scenarios, but using GrantPermissionRule for this will not work, since in this case the permission will always be approved, and in tests we will never know what the behavior will be if the request is denied.

Testing with Device.Permissions

One way to solve the problem is to interact with the dialogue using Kautomator, having previously found all the necessary interface elements.

Kautomator is a component in Kaspresso that allows you to interact with third-party applications and system dialogs. You can read more about it in our tutorial and in official documentation.

In this situation, solving the problem through Kautomator is not the best solution; a much more convenient method has been added to Kaspresso – Device.Permissions. It makes it very easy to check permission dialogs and agree or reject them.

Therefore, instead of Rule, we will use the Permissions object, which can be obtained from Device. Let's do this in a separate class so that you have both test options. Let's rename the class we are currently working in to MakeCallActivityRuleTest.

And create a new class MakeCallActivityDevicePermissionsTest. The code can be copied from the current test, except for the 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)
            }
        }
    }
}

If we run the test now, it will fail because we have not given permissions to make calls. Let's add one more step in which we will give the appropriate permission through device.permissions. After specifying an object, you can put a dot and see what methods it has:

It is possible to check whether the dialog is displayed, and also to reject or allow it.

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

This way we will make sure that the dialogue is displayed and give consent to make calls.

We remind you that the dialog will be shown on Android API version 23 and higher; we will discuss how to perform these tests on earlier versions at the end of this article.

Here we wrote device.permissions twice, let's shorten the code a little by using function [apply]. Let’s also put the check via assert into the flakySafely method. Then the entire test code will look like this:

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)
            }
        }
    }
}

Let's launch. The test was completed successfully.

Now we can easily write a test to ensure that a call is not made if permission has not been given. To do this, instead of allowViaDialog you need to specify denyViaDialog.

You also need to change the checks in the test itself, and do not forget to remove the code from the after function in the new method, since after the permission is rejected, the call will not be made and after the test there is no longer a need to reset the call.

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()
            }
        }
    }
}

Testing on different API versions

On modern versions of the Android OS (API 23 and higher), permissions are requested from the user while the application is running through a dialog. But in earlier versions, they were requested at the time the application was installed, and during operation it was assumed that the user had agreed to all the required permissions.

Therefore, if you run a test on devices with an API lower than version 23, then there will be no request for permissions, and therefore no checking of the dialog is required.

In a test using GrantPermissionRule, no changes are required; older versions always have permission, so this annotation will not affect the operation of the test in any way. But in the test using device.permissions, changes must be made, since here we are explicitly checking the operation of the dialog.

There are several options here. Firstly, on such devices there is no point in checking the operation of the application if the permission was denied, so this test should simply be skipped. To do this, you can use the @SuppressSdk annotation. Then the checkCallIfPermissionDenied method code will change to:

@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()
        }
    }
}

Now this test will only be performed on new versions of the Android OS, and will be skipped on older ones.

The second solution to the problem is to skip certain steps or replace them with others depending on the API level. For example, in the checkSuccessCall method on older devices, we can skip the step of checking the dialog; to do this, use the following code:

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

The rest of the code can be left untouched and the test will run successfully on both new and old devices, it’s just that in one case permission will be requested, in the other it will not.

The final test code will now look like this:

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()
            }
        }
    }
}

Bottom line

In this article, we looked at two options for working with Permissions: GrantPermissionRule and device.permissions.

We also learned that the second option is preferable for a number of reasons:

  1. The Permissions object allows you to check whether the permission dialog is displayed.
  2. When using Permissions, we can test the application's behavior not only when accepting a permission, but also when rejecting it.
  3. Tests using GrantPermissionRule will not work if the permission has been previously denied. You will need to reinstall the application or cancel previously granted permissions using the adb shell command.
  4. If you revoke permission using the adb shell command while the test is running, then if you use the Permissions object, the test will work correctly, but if you use GrantPermissionRule, it will crash.

If you liked the capabilities of the framework and you plan to use this library in your projects, then join

Kaspresso community on Telegram

. There we will try to provide you with all the necessary support and will announce the following articles from the current series of materials about Kaspresso.

And don't forget give a star on Github 😉

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *