Originally posted on the 15th of july 2019 on my own blog.
While working on an Android application I wanted to test permission requests. This application needed the location permission, typically this is code that is written once and when it is marked as done never looked at again. I wanted to create some UI tests to ensure that everything works and will still work in the future as expected.
Let's start this off with creating a sample application. It's written in Kotlin with API 23 as minimum API level and it also uses the androidx.* artifacts.
Following permissions are added in the AndroidManifest.xml
file:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
When the activity is created, the app will check if the location permissions have been granted. If they haven't been granted yet, the app will request them. When the permissions are denied, the app will show Permissions denied
to the user. When the permissions are granted, it will show Permissions granted
. Pretty straightforward.
This is the overridden onCreate
method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
), LocationPermissionRequestCode)
}
}
This code checks if the ACCESS_FINE_LOCATION
and the ACCESS_COARSE_LOCATION
permissions have been granted. If not they will be requested from the user. The LocationPermissionRequestCode
variable is a constant to identify the request. In the sample application it is set to 1
.
Next we need to handle the response of the user. To do this, there is a method which the activity provides that can be overridden. The onRequestPermissionsResult
method is overridden and a switch statement determines for which request the response is received. Note that the same LocationPermissionRequestCode
variable is used as above. According to the result of the request the text of the feedbackLabel
is changed.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when(requestCode) {
LocationPermissionRequestCode -> {
if((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
feedbackLabel.text = "Permissions granted"
} else {
feedbackLabel.text = "Permissions denied"
}
return
}
else -> {
}
}
}
Note: to access the feedbackLabel
I'm using the Kotlin Android Extensions.
Now the sample app is working let's get to the testing part. Code like this is often written once, tested once and then never touched again. A developer will grant the permissions and go on with his life. If a bug creeps in it will be noticed too late or not at all. So let's write some UI tests to verify that the app behaves as expected. UI testing on Android is often done using Espresso and it works quite well. There even is an Espresso test recorder built into Android Studio to quickly record tests.
Let's record our first Espresso test, if the user denies the permission the feedbackLabel
should read Permissions denied
. In Android Studio select Run > Record Espresso Test
from the menu.
The test recorder will open and a log of all actions that have happened on the device will appear. After clicking the Deny
button assert that the feedbackLabel
has the expected value.
If Espresso has not been added to the gradle file of the application, Android Studio will propose to do it for you.
Unfortunately Espresso can't be used for this kind of test. Espresso only works on the current package and the Allow
and Deny
buttons from the Permissions dialog are from another package. As you can see in the test record, the click on the Deny
button is not recorded to the list of actions.
There is however a solution, next to Espresso there is also the UI Automator testing framework. This framework has no problem with accessing elements from another package. We can't use the test recorder so the test will have to be coded but this is pretty straightforward. This line in the gradle file will add UI Automator:
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
And the test becomes:
@RunWith(AndroidJUnit4::class)
class MainActivityTests {
private var device : UiDevice? = null
@get:Rule
var mainActivityTestRule = ActivityTestRule(MainActivity::class.java)
@Before
fun setUp() {
this.device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun testFeedbackLocationPermissionDenied() {
val denyButton = this.device?.findObject(UiSelector().text("DENY"))
val permissionDeniedMessage = this.device?.findObject(UiSelector().text("Permission denied"))
denyButton!!.click()
assert(permissionDeniedMessage!!.exists())
}
@Test
fun testFeedbackLocationPermissionAllowed() {
val allowButton = this.device?.findObject(UiSelector().text("ALLOW"))
var permissionAllowedMessage = this.device?.findObject(UiSelector().text("Permission allowed"))
allowButton!!.click()
assert(permissionAllowedMessage!!.exists())
}
}
The testFeedbackLocationPermissionAllowed
test will check the message when the permission is allowed, the testFeedbackLocationPermissionDenied
will check the message when the permission is denied.
There is however a problem with these tests. Once the permission has been allowed, the tests will fail when they are run again. If you think about it, it's not surprising. Because the permission is already granted, the permission popup will no longer be shown the second, third, fourth, ... time the tests are run.
To fix this the granted permissions need to be cleared every time the tests are run. Clearing the permission can be done using pm revoke
. These commands are added to the After
method to clean up the permissions.
@After
fun tearDown() {
InstrumentationRegistry.getInstrumentation().uiAutomation.
executeShellCommand("pm revoke ${InstrumentationRegistry.getInstrumentation().targetContext.packageName} android.permission.ACCESS_COARSE_LOCATION")
InstrumentationRegistry.getInstrumentation().uiAutomation.
executeShellCommand("pm revoke ${InstrumentationRegistry.getInstrumentation().targetContext.packageName} android.permission.ACCESS_FINE_LOCATION")
}
}
When the tearDown
executes logcat logs the following:
2019-07-15 11:09:43.639 21138-21154/com.eysermans.permissionuitesting W/UiAutomation: UiAutomation.revokeRuntimePermission() is more robust and should be used instead of 'pm revoke'
So instead of using pm revoke
we should use revokeRuntimePermission
.
@After
fun tearDown() {
InstrumentationRegistry.getInstrumentation().uiAutomation.revokeRuntimePermission(
InstrumentationRegistry.getInstrumentation().targetContext.packageName,
Manifest.permission.ACCESS_COARSE_LOCATION)
InstrumentationRegistry.getInstrumentation().uiAutomation.revokeRuntimePermission(
InstrumentationRegistry.getInstrumentation().targetContext.packageName,
Manifest.permission.ACCESS_FINE_LOCATION)
}
Unfortunately this didn't work either. Every time the testFeedbackLocationPermissionAllowed
test ran it failed stating that more information can be found in logcat. However I did not find any additional error logging in logcat. It just did not work.
Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.''. Check device logcat for details
Let's explore another option, using the Android Test Orchestrator. Add the dependency to the gradle file:
androidTestUtil 'androidx.test:orchestrator:1.2.0'
Then add the following line to the defaultConfig
section of the gradle file:
testInstrumentationRunnerArguments clearPackageData: 'true
And add the testOptions
section to the android
section:
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests {
includeAndroidResources = true
}
}
The gradle file now looks like this:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.eysermans.permissionuitesting"
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:rules:1.3.0-alpha01'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
}
The tearDown
method can now completely be removed and the testFeedbackLocationPermissionAllowed
method can now be run over and over again.
It took some work but this solution works. The permissions are cleared before every test. I preferred using a tearDown
method because it is more straightforward. But unfortunately it did not work as expected.
The sample application is available on GitHub.
More links on the subject:
Top comments (0)