使用 Mock 修改 Android 设备的 features

背景

手工测试过程中有个测试场景需要删除测试设备上某个Android feature,往上搜索了一圈没找到有效的操作方法。获取Android设备所有的feature可以通过adb命令pm list features或者Android代码Context.getPackageManager().getSystemAvailableFeatures(),但都没有对应的修改方法。

既然feature是从Context获取的,那能不能构造一个只包含我想要的feature的Context呢。顺着这个思路,有了下面的方案。

方案设计

单元测试常用的Mock技术,就是来构造假/模拟对象的。但如果完全构造,又担心和真实环境差别较大,测试结果不可靠。能不能从真实Android设备中获取真实的Context,把不想要的feature去除,再给到被测试方法中呢?

答案是肯定的,通过选用流行的Mock组件Mockito ,官网上给出了下面2种Mock Java对象的方式:

  1. mock()/@Mock: create mock
  2. spy()/@Spy: partial mocking, real methods are invoked but still can be verified and stubbed

可以看到Spy这种Mock方式可完美地解决我的需求。

实现(简化版需求)

业务需求说明

获取所有Android Features并把feature name打印在日志中,使用adb命令的效果如下:

➜  study git:(master) ✗ adb shell pm list features                                    
feature:reqGlEsVersion=0x30002
feature:android.hardware.audio.output
feature:android.hardware.bluetooth
feature:android.hardware.bluetooth_le
feature:android.hardware.camera
feature:android.hardware.camera.any
feature:android.hardware.camera.autofocus
feature:android.hardware.camera.capability.manual_post_processing
feature:android.hardware.camera.capability.manual_sensor
feature:android.hardware.camera.capability.raw
feature:android.hardware.camera.flash
feature:android.hardware.camera.front
feature:android.hardware.camera.level.full
feature:android.hardware.faketouch
feature:android.hardware.location
feature:android.hardware.location.gps
feature:android.hardware.location.network
feature:android.hardware.microphone
feature:android.hardware.nfc.any
feature:android.hardware.opengles.aep
feature:android.hardware.ram.normal
feature:android.hardware.screen.landscape
feature:android.hardware.screen.portrait
feature:android.hardware.sensor.accelerometer
feature:android.hardware.sensor.compass
feature:android.hardware.sensor.light
feature:android.hardware.sensor.proximity
feature:android.hardware.sensor.stepcounter
feature:android.hardware.telephony
feature:android.hardware.telephony.cdma
feature:android.hardware.telephony.gsm
feature:android.hardware.touchscreen
feature:android.hardware.touchscreen.multitouch
feature:android.hardware.touchscreen.multitouch.distinct
feature:android.hardware.touchscreen.multitouch.jazzhand
feature:android.hardware.usb.accessory
feature:android.hardware.usb.host
feature:android.hardware.vulkan.compute
feature:android.hardware.vulkan.level
feature:android.hardware.vulkan.version=4194307
feature:android.hardware.wifi
feature:android.hardware.wifi.direct
feature:android.software.activities_on_secondary_displays
feature:android.software.app_widgets
feature:android.software.autofill
feature:android.software.backup
feature:android.software.companion_device_setup
feature:android.software.connectionservice
feature:android.software.cts
feature:android.software.device_admin
feature:android.software.file_based_encryption
feature:android.software.input_methods
feature:android.software.live_wallpaper
feature:android.software.managed_users
feature:android.software.midi
feature:android.software.picture_in_picture
feature:android.software.print
feature:android.software.securely_removes_users
feature:android.software.sip
feature:android.software.sip.voip
feature:android.software.verified_boot
feature:android.software.voice_recognizers
feature:android.software.webview
业务需求代码实现
public class FeaturesUtil {
    private static final String TAG = "FeaturesUtil";

    public static void getFeatures(Context context) {
        PackageManager packageManager = context.getPackageManager();
        FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures();

        for (FeatureInfo featureInfo : featureInfos) {
            Log.i(TAG, "feature: " + featureInfo.name);
        }
    }
}
测试代码实现(去除bluetooth feature)
    @Spy
    Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();;

    @Spy
    PackageManager packageManager = context.getPackageManager();

		@Test
    public void getFeatures() {
        // 去除bluetooth feature
        FeatureInfo[] mockedFeatureInfos = removeFeature(packageManager.getSystemAvailableFeatures(),
                Arrays.asList("android.hardware.bluetooth", "android.hardware.bluetooth_le"));

        when(packageManager.getSystemAvailableFeatures()).thenReturn(mockedFeatureInfos);
        when(context.getPackageManager()).thenReturn(packageManager);

        FeaturesUtil.getFeatures(context);
    }

    /**
     * 根据feature name删除一个或多个feature
     *
     * @param featureInfos FeatureInfo数组
     * @param featureNames 字符串列表,每个元素是要删除的FeatureInfo的name
     * @return 删除之后的FeatureInfo数组
     */
    private FeatureInfo[] removeFeature(FeatureInfo[] featureInfos, List<String> featureNames) {
        List<FeatureInfo> featureInfoList = new ArrayList();

        for (FeatureInfo featureInfo :
             featureInfos) {
            if (featureInfo.name != null && !(featureNames.contains(featureInfo.name))) {
                featureInfoList.add(featureInfo);
            }
        }

        return featureInfoList.toArray(new FeatureInfo[featureInfoList.size()]);
    }
运行测试代码后Logcat输出

可以看到bluetooth相关的feature已经没有了

2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.sensor.proximity
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.sensor.accelerometer
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.faketouch
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.usb.accessory
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.telephony.cdma
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.backup
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.touchscreen
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.touchscreen.multitouch
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.print
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.activities_on_secondary_displays
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.voice_recognizers
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.picture_in_picture
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.opengles.aep
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.autofocus
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.telephony.gsm
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.sip.voip
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.usb.host
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.audio.output
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.verified_boot
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.flash
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.front
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.screen.portrait
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.microphone
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.autofill
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.securely_removes_users
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.sensor.compass
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.hardware.touchscreen.multitouch.jazzhand
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.app_widgets
2021-04-07 13:23:39.266 16238-16268/? I/FeaturesUtil: feature: android.software.input_methods
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.sensor.light
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.vulkan.version
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.companion_device_setup
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.device_admin
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.screen.landscape
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.ram.normal
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.managed_users
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.webview
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.sensor.stepcounter
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.capability.manual_post_processing
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.any
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.capability.raw
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.vulkan.compute
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.connectionservice
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.touchscreen.multitouch.distinct
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.location.network
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.cts
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.sip
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.capability.manual_sensor
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.camera.level.full
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.wifi.direct
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.live_wallpaper
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.location.gps
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.software.midi
2021-04-07 13:23:39.267 16238-16268/? I/FeaturesUtil: feature: android.hardware.nfc.any
2021-04-07 13:23:39.268 16238-16268/? I/FeaturesUtil: feature: android.hardware.wifi
2021-04-07 13:23:39.268 16238-16268/? I/FeaturesUtil: feature: android.hardware.location
2021-04-07 13:23:39.268 16238-16268/? I/FeaturesUtil: feature: android.hardware.vulkan.level
2021-04-07 13:23:39.268 16238-16268/? I/FeaturesUtil: feature: android.hardware.telephony
2021-04-07 13:23:39.268 16238-16268/? I/FeaturesUtil: feature: android.software.file_based_encryption
2021-04-07 13:23:39.269 16238-16268/? I/TestRunner: finished: getFeatures(com.aniu.featuresmock.FeaturesUtilTest)

总结

  • 不直接使用Mock,而是使用Spy,最大限度保持测试环境真实可靠,从而保证测试效果

  • 测试代码不要放在test目录,要放在androidTest目录,保证在真实设备上跑(不然代码也会报错)。目录结构如下:
    在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Mock 是一种用于在单元测试中模拟 Android 框架的工具。它允许开发人员模拟 Android 组件的行为,以便更容易地进行单元测试。 要使用 Android Mock,您需要添加以下依赖项到您的项目中的 build.gradle 文件中: ``` testImplementation 'org.mockito:mockito-core:x.x.x' ``` 其中 x.x.x 是最新版本的 Mockito。 接下来,您可以在测试类中使用 Mockito 创建和配置模拟对象。例如,假设您要测试一个依赖于网络请求的类: ```java public class MyNetworkClass { private NetworkService networkService; public MyNetworkClass(NetworkService networkService) { this.networkService = networkService; } public void fetchData() { // 使用 NetworkService 执行网络请求 networkService.makeRequest(); } } ``` 您可以使用 Mockito 创建一个模拟的 NetworkService,并对其进行配置,以便在测试中指定预期行为: ```java import org.mockito.Mockito; public class MyNetworkClassTest { @Test public void testFetchData() { // 创建模拟对象 NetworkService mockNetworkService = Mockito.mock(NetworkService.class); // 配置模拟对象的行为 Mockito.when(mockNetworkService.makeRequest()).thenReturn("Mock response"); // 创建要测试的对象,并将模拟对象注入其中 MyNetworkClass myNetworkClass = new MyNetworkClass(mockNetworkService); // 执行测试代码 myNetworkClass.fetchData(); // 验证模拟对象的方法是否被调用,并检查预期结果 Mockito.verify(mockNetworkService).makeRequest(); } } ``` 在此示例中,我们创建了一个模拟的 NetworkService 对象,并配置了它的 makeRequest() 方法的行为。在测试中,我们创建了要测试的 MyNetworkClass 对象,并将模拟的 NetworkService 对象注入其中。然后,我们执行 fetchData() 方法,并使用 Mockito.verify() 来验证模拟对象的方法是否被调用。 这只是一个简单的示例,Android Mock 还提供了更多功能和灵活性,例如模拟异步任务、回调等。您可以通过查阅 Mockito 的文档以及 Android Mock 的其他相关资源来深入了解和使用 Android Mock

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值