Android SDK 及native方法 单元测试

举报
小蒯 发表于 2020/08/17 16:19:12 2020/08/17
【摘要】 单元测试就是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。 对于Android SDK来说,我们的单元测试,除了覆盖对外的接口方法外,主要是针对测试人员测试不到的一些内部逻辑方法。同时,可以在转测之前让程序自动运行自测。

    单元测试就是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。

    对于AndroidSDK来说,我们的单元测试,主要是针对测试人员测试不到的一些内部逻辑方法。同时,可以在转测之前让程序自动运行自测。

一、单元测试常用的覆盖率量化标准

一般来说路径覆盖率>判定覆盖率>语句覆盖率

1、语句覆盖/行覆盖

这是一种比较常用的指标,度量的是被测试代码中所有可执行语句是否被执行到,单独一行的花括号{}也常常被统计进去。

语句覆盖只管覆盖代码中的可执行语句,但是没有考虑各种分支的组合,常常被指为“最弱的覆盖”。举个例子,被测试代码如下:

int foo(int a, int b) {
   
return  a / b;
}

只要设计如下一组测试用例,就可以达到覆盖率100%:

TeseCase: a = 10, b = 5

但是这里有一个比较简单的bug,当b=0的时候,程序就会抛出异常。所以语句覆盖率这个量化标准,并不能直接的表示代码的功能性完整。

2、判定覆盖/分支覆盖

它度量程序中每一个判定的分支是被测试到了,容易与条件覆盖混淆,所以在条件覆盖中对比说明。

3、条件覆盖率

它度量判定中的每个子表达式结果true和false是否被测试到了。举一个例子来区分条件覆盖和判定覆盖,被测试代码如下:

int foo(int a, int b) {

    if (a < 10 || b < 10) { // 判定
        return 0;  // 分支一
    }
    else {
        return 1;  // 分支二
    }
}

设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,我们设计如下的案例就能达到判定覆盖率100%:

TestCaes1: a = 5, b = 任意数字  // 覆盖了分支一
TestCaes2: a = 15, b = 15         // 覆盖了分支二

设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,我们设计了如下的案例:

TestCase1: a = 5, b = 5       // true,  true
TestCase4: a = 15, b = 15   // false, false

通过上面的例子,我们应该很清楚了判定覆盖和条件覆盖的区别。需要特别注意的是:条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就OK了。因此,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。比如上面的例子,假如我设计的案例为:

TestCase1: a = 5, b = 15   // true,  false   分支一
TestCase1: a = 15, b = 5   // false, true    分支一

我们看到,虽然我们完整的做到了条件覆盖,但是我们却没有做到完整的判定覆盖,我们只覆盖了分支一。

4、路径覆盖/断言覆盖

它度量了是否函数的每一个分支都被执行了。举个例子区分一下四种覆盖方式,被测试代码如下:

int foo(int a, int b) {
    
int nReturn = 0;
    
if (a < 10) { // 分支一
        nReturn += 1;
    }
    
if (b < 10) { // 分支二
        nReturn += 10;
    }
    
return nReturn;
}

a.语句覆盖

TestCase a = 5, b = 5   // nReturn = 11

b.判定覆盖

TestCase1 a = 5,   b = 5     // nReturn = 11

TestCase2 a = 15, b = 15   // nReturn = 0

c.条件覆盖

TestCase1 a = 5,   b = 15   // nReturn = 1

TestCase2 a = 15, b = 5     // nReturn = 10

d.路径覆盖

TestCase1 a = 5,    b = 5     // nReturn = 0

TestCase2 a = 15,  b = 5     // nReturn = 1

TestCase3 a = 5,    b = 15   // nReturn = 10

TestCase4 a = 15,  b = 15   // nReturn = 11

二、工具选择:JUnit+PowerMockito

    JUnit只能mock方法,不能mock实例;并且JUnit使用本地JVM提供运行环境,如果测试的单元依赖了Android框架(或者测试类依赖于其他外部类的情况),比如用到了Android中的Context、SP类的一些方法,本地JVM将无法提供这样的环境,所以我们一般会结合其他开源Mock框架共同完成单元测试。

    Android(Java)常用的单元测试开源库一般是Mockito模拟框架,Mockito可以Mock对象,同时可以模拟安卓的运行环境,以及一些service访问返回等。但是,Mokito只能模拟public的方法(网上有通过反射修改private方法access来实现mock方法的,感觉比较麻烦)比较适用于大多标准的单元测试case。

    PowerMockito是一个扩展了其他mock框架的、功能更强大的框架。与Mockito的用处总体上一样,都是为了mock外部的、不容易构造的环境,但是PowerMokito可以用于解决更多复杂的情况,PowerMockito可以对private/final/static的方法进行mock,同时PowerMockito支持Mockito和EasyMock。

三、常用方法

模糊匹配

想测试一个方法,但是不想传入精确的值,这时候可以使用Mockito.antInt(),Mockito.anyString()方法。比如我们的日志功能Log.d(tag,msg),我们想mock这个方法,不论传入的tag和msg是什么字符串,就可以使用这个方法,后面的方法会有具体的代码示例。

模拟static方法

代码中比较常见的就是自定义的static日志方法,Log.d(tagStr, mesStr),测试一个方法的时候,如果里面有日志输出,可以在方法调用之前,mock当调用日志方法的时候,什么都不做,代码如下:

PowerMockito.mockStatic(Log.class);
try {
    PowerMockito.doNothing().when(Log.class, "d", Mockito.anyString(), Mockito.anyString());
} catch (Exception e) {
    e.printStackTrace();
}

如果是有返回值得static方法,则可能用PowerMockito.doReturn(mockValue).when()或者PowerMockito.when().thenReturn(mockValue)来模拟。

如果使用了mockStatic方法,需要在class前面加注解,代码如下:

@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})

理论上来说,@PrepareForTests注解也可以放在对应的测试方法上方,但是有时候会报错,所以最好还是统一放在class前面,里面包含了所有需要mock的class。

private方法的单元测试

基本实现方式是通过反射,将private方法的accessible设为true,之后与正常的public方法的单元测试方法相同。比如待测试类Tested中有待测试方法如下:

private long getPrivateMethod(SelfClass self) {
    long result = -1;
    if (self.methodA()) {
        result = 1;
    }
    return result;
}


单元测试方法中,反射代码如下:

Tested test = new Tested();
Method getPrivate = test.getClass().getDeclaredMethod("getPrivateMethod", Tested.class);
getPrivate.setAccessible(true);

native方法的单元测试

native方法的单元测试从运行环境上也是有两种实现方式:本地单元测试和真机(仪器化)测试。本地单元测试,运行在JVM上,需要依赖于Espresso等模拟安卓环境的工具,好处是运行速度较快,但是仿真度稍差,同时比较麻烦。仪器化测试运行在真机上,好处是仿真度高,同时比较方便,缺点是,仪器化测试除了打包本身的apk外,还会生成test-apk(SDK的话,只会生成test-apk),因为要生成apk,所以速度比较慢。

仪器化测试需要将测试代码写在androidTest目录下,如果native方法依赖于本地的so库,则需要以相同的目录结构,在androidTest目录下放置so文件。仪器化测试一般用的是Android自带的AndroidJUnitRunner,不过目前不支持PowerMockito,依赖隔绝不太方便,所以建议尽量使用本地测试,对于依赖于不好模拟的安卓环境,或者耗时较高的文件读写等方法,再使用真机测试。


四、IDEA(AndroidStudio)配置依赖

配置build.gradle文件:

dependencies {
......

testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.8.0'
testImplementation 'org.powermock:powermock-module-junit4:1.7.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.1'
testImplementation 'org.powermock:powermock-api-mockito2:1.7.1'

androidTestImplementation 'com.android.support:support-annotations:24.0.0'
androidTestImplementation 'com.android.support.test:runner:0.5'
......

}

需要注意PowerMockito和Mockito的版本对应,否则可能会报java.lang.ClassNotFoundException: org.mockito.exceptions.Reporter


testImplementation 'org.powermock:powermock-api-mockito2:1.7.1'

上面的配置需要注意是mockito2。(之前每次使用PowerMockito.doXXX.when()的时候都会报错,升级版本之后就没有了,不确定是不是因为版本的问题,还是这个2的问题)

五、覆盖率检测及报告生成:JaCoCo

六、可能出现的问题

UnfinishedStubbingException

1、条件里面嵌套的方法比较多

网上的示例基本都是说,when里面的条件比较复杂,就会嵌套调用了方法,可以把方法放到外面来做,网上举的例子错误代码如下:

@Test

public myTest({

    MyMainModel mainModel = Mockito.mock(MyMainModel.class);

    Mockito.when(mainModel.getList()).thenReturn(getSomeList());

 }

修改后代码为:

@Test

public myTest({

    MyMainModel mainModel = Mockito.mock(MyMainModel.class);

    List<SomeModel> someModelList = getSomeList();

    Mockito.when(mainModel.getList()).thenReturn(someModelList);

}

网上基本上查到的答案都是上面那个解决方案,不过实际原因可能比较多,还是以Log.d(tag, msg)方法举例:

2、条件语句格式的问题

PowerMockito.doNothing().when()或者PowerMockito.doReturn.when()的时候,when里面的条件写法也可能导致报错,还是以Log.d(tag, msg)举例:

// 错误示例

PowerMockito.doNothing().when(log.d(tag, msg));

PowerMockito.doNothing().when(log).d(tag, msg);

PowerMockito.doNothing().when(log.class, "d", String.class, String.class);

// 正确示例

PowerMockito.doNothing().when(Log.class, "d", Mockito.anyString(), Mockito.anyString());

PowerMockito.doNothing().when(Log.class, "d", "tag", "message");

// 这个写法在方法里面写没有问题,但是写在@Before或者@BeforeClass里面就会报错,不知道为啥

PowerMockito.doNothing().when(Log.class);

类要用class对象而不是具体的实例;

通过String的格式传入方法名,此处需要try-catch,因为可能找不到这个方法名的方法;

参数要传入具体的实例,或者模糊替代,否则会报找不到对应的方法的错误。

找不到Declared的方法

参考UnfinishedStubbingException里面条件语句格式问题。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。