Android SDK 及native方法 单元测试
单元测试就是针对最小的功能单元编写测试代码。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里面条件语句格式问题。
- 点赞
- 收藏
- 关注作者
评论(0)