Android Marshmallow:使用 Espresso 测试权限?

Android Marshmallow: Test permissions with Espresso?

Android Marshmallow 引入的新权限方案需要在 运行 时检查特定权限,这意味着需要根据用户是拒绝还是允许访问来提供不同的流程。

当我们使用 Espresso 对我们的应用进行 运行 自动化 UI 测试时,我们如何模拟或更新权限状态以测试不同的场景?

实际上到目前为止我知道有两种方法可以做到这一点:

  1. 在测试开始前使用 adb 命令授予权限 (documentation):

adb shell pm grant "com.your.package" android.permission.your_permission

  1. 您可以单击权限对话框并使用 UIAutomator (documentation) 设置权限。如果您的测试是使用 Espresso 为 android 编写的,您可以轻松地将 Espresso 和 UIAutomator 步骤组合在一个测试中。

当您的 phone 是英语语言环境时,尝试使用这种静态方法:

private static void allowPermissionsIfNeeded() {
    if (Build.VERSION.SDK_INT >= 23) {
        UiDevice device = UiDevice.getInstance(getInstrumentation());
        UiObject allowPermissions = device.findObject(new UiSelector().text("Allow"));
        if (allowPermissions.exists()) {
            try {
                allowPermissions.click();
            } catch (UiObjectNotFoundException e) {
                Timber.e(e, "There is no permissions dialog to interact with ");
            }
        }
    }
}

找到了here

已接受的答案实际上并未测试权限对话框;它只是绕过它。因此,如果权限对话框由于某种原因失败,您的测试将给出错误的绿色。我鼓励实际点击 "give permissions" 按钮来测试整个应用程序的行为。

看看这个解决方案:

public static void allowPermissionsIfNeeded(String permissionNeeded) {
    try { 
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasNeededPermission(permissionNeeded)) {
        sleep(PERMISSIONS_DIALOG_DELAY);
        UiDevice device = UiDevice.getInstance(getInstrumentation());
        UiObject allowPermissions = device.findObject(new UiSelector()
          .clickable(true) 
          .checkable(false) 
          .index(GRANT_BUTTON_INDEX));
        if (allowPermissions.exists()) {
          allowPermissions.click();
        } 
      } 
    } catch (UiObjectNotFoundException e) {
      System.out.println("There is no permissions dialog to interact with");
    } 
  } 

在此处找到整个 class:https://gist.github.com/rocboronat/65b1187a9fca9eabfebb5121d818a3c4

顺便说一句,由于这个答案很受欢迎,我们将 PermissionGranter 添加到 Barista,这是我们在 Espresso 和 UiAutomator 之上的工具,使仪器测试绿色化: https://github.com/SchibstedSpain/Barista 检查一下,因为我们会逐个版本维护它。

您可以在测试 运行 之前授予权限,例如:

@Before
public void grantPhonePermission() {
    // In M+, trying to call a number will trigger a runtime dialog. Make sure
    // the permission is granted before running this test.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        getInstrumentation().getUiAutomation().executeShellCommand(
                "pm grant " + getTargetContext().getPackageName()
                        + " android.permission.CALL_PHONE");
    }
}

但是你不能撤销。如果您尝试 pm reset-permissionspm revoke... 进程将被终止。

您可以通过在开始测试前授予权限来轻松实现此目的。例如,如果您要在测试期间使用摄像头运行,您可以按如下方式授予权限

@Before
public void grantPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        getInstrumentation().getUiAutomation().executeShellCommand(
                "pm grant " + getTargetContext().getPackageName()
                        + " android.permission.CAMERA");
    }
}

随着 Android Testing Support Library 1.0, there's a GrantPermissionRule 的新版本,您可以在测试中使用它在开始任何测试之前授予权限。

@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);

Kotlin 解决方案

@get:Rule var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)
必须使用

@get:Rule 以避免 java.lang.Exception: The @Rule 'permissionRule' must be public. 更多信息 .

我知道一个答案已被接受,但是,不是一遍又一遍地建议的 if 语句,另一种更优雅的方法是在您想要的实际测试中执行以下操作OS 的特定版本:

@Test
fun yourTestFunction() {
    Assume.assumeTrue(Build.VERSION.SDK_INT >= 23)
    // the remaining assertions...
}

如果调用 assumeTrue 函数时表达式的值为 false,测试将停止并被忽略,我假设这就是您想要的,以防测试是在 SDK 之前的设备上执行的23.

ESPRESSO 更新

This single line of code grants every permission listed as parameter in the grant method with immediate effect. In other words, the app will be treated like if the permissions were already granted - no more dialogs

@Rule @JvmField
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)

和gradle

dependencies {
  ...
  testImplementation "junit:junit:4.12"
  androidTestImplementation "com.android.support.test:runner:1.0.0"
  androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.0"
  ...
}

参考:https://www.kotlindevelopment.com/runtime-permissions-espresso-done-right/

我已经实现了一个利用包装器 classes 的解决方案,覆盖并构建变体配置。解决方案解释起来很长,可在此处找到:https://github.com/ahasbini/AndroidTestMockPermissionUtils.

它还没有打包在 sdk 中,但主要思想是覆盖 ContextWrapper.checkSelfPermissionActivityCompat.requestPermissions 的功能以进行操作,并且 return 模拟结果欺骗应用程序进入要测试的不同场景如下:权限被拒绝因此应用程序请求它并以授予权限结束。即使应用程序一直具有权限,这种情况也会发生,但想法是它被覆盖实现的模拟结果所欺骗。

此外,该实现还有一个名为 PermissionRule class 的 TestRule,可用于测试 classes 以轻松模拟所有条件以测试权限无缝地。也可以做出断言,例如确保应用程序调用了 requestPermissions()

Android 测试支持库 中有 GrantPermissionRule,您可以在测试中使用它在开始任何测试之前授予权限。

@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant(android.Manifest.permission.CAMERA, android.Manifest.permission.ACCESS_FINE_LOCATION);

感谢@niklas 提供的解决方案。如果有人希望在 Java:

中授予多个权限
 @Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.CAMERA);

如果您需要为单个测试或在运行时设置权限而不是规则,您可以使用此:

PermissionRequester().apply {
    addPermissions(android.Manifest.permission.RECORD_AUDIO)
    requestPermissions()
}

例如

@Test
fun openWithGrantedPermission_NavigatesHome() {
    launchFragmentInContainer<PermissionsFragment>().onFragment {
        setViewNavController(it.requireView(), mockNavController)
        PermissionRequester().apply {
            addPermissions(android.Manifest.permission.RECORD_AUDIO)
            requestPermissions()
        }
    }

    verify {
        mockNavController.navigate(R.id.action_permissionsFragment_to_homeFragment)
    }
}

允许权限,必要的时候,我觉得最简单的方法就是在需要这个权限的测试中直接使用BaristaPermissionGranter.allowPermissionsIfNeeded(Manifest.permission.GET_ACCOUNTS)