Android平台下的单元测试
前言
对于一个快速开发迭代的Android App来说,为了在频繁的修改与增加代码的过程中使代码有更加可靠的质量保障,需要对项目中较为重要的模块进行黑盒测试。故需要引入单元测试对周期性地对引擎进行函数级别的测试。当测试代码能够拥有良好的覆盖率下,能够更加确保代码的质量。
单元测试的本质
单元测试本质是验证函数功能的正常,它测试的主要目标函数有三种:
- 有明确的返回值。对于给定的输入值,能有确定的返回值。
- 没有返回值,但会有回调。
- 没有返回值也没有回调,但改变了某些状态与属性。
对于其他类型的函数,单元测试是无法进行测试的,理论上不应该在项目中存在。当出现这一特性的函数时,应该采用一个或多个case对一个函数进行测试,每个case对于不同的条件输入,验证其结果是否符合预期。相应的流程图如下:
单元测试应该具有的品质
- 可靠性(测试成功了,就说明产品代码没有问题,测试失败了,就说明产品代码写错了(前提是产品代码没有被修改过),无需怀疑是测试代码的问题)
- 可维护性(抽取公共方法、测试项之间没有依赖性等等)
- 可读性(这个就是具体的代码规范)
Android平台上的单元测试框架
在Android中,单元测试框架主要为成熟的Junit框架,而由于项目依赖Android平台,故还需要平台级别框架支持,主流的有AndroidTest框架与Robolectric框架。前者测试执行时需要依赖Android环境。而后者则在JVM上构造了一个虚拟的Android运行环境,可以脱离出Android平台,速度也更快,并且可由Jenkins周期性执行。
在单元测试过程中,需要测试的函数经常调用了其他地方的一些函数与方法,且函数执行结果依赖于其他函数的调用返回结果。这违反了单元测试的单一职责测试的原则,故我们需要使用mock框架,将函数依赖项的返回值修改为我们想要的返回值。目前比较成熟的java平台上的mock框架为Mockito框架,而Mockito框架的扩展PowerMock框架则增加了静态方法与final类的mock功能。
单元测试框架要求
- 编写测试用例尽量简单,上手容易
- 提供基础类和接口
- 含有代码级别的标记,用于标记测试方法的属性,@Test
- 提供断言类和验证方法,用于验证代码
- 可以控制测试的执行策略
- 自动运行,可以自动化配置
- 可以显示详细的运行结果报告,包括测试总数、通过数、失败测试的原因
单元测试的流程
简要流程图如下:
在建立单元测试case列表时,要注意实现条件覆盖与路径覆盖,这里涉及到了计算程序环路复杂性的问题(纯属个人见解):V(G) = e - n + 2。其中e为边数,n为节点数。V(G)的值即代表程序中独立路径的条数,对应case的个数。下面,我们以一个简单的sdk下载更新流程为例:
示例:一个SDK的下载更新流程
待测试代码如下:
public class UpdateExample {
/**
* 更新sdk
* @return 是否更新成功
*/
public boolean updateSdk() {
// 获取本地sdk version
int localVersion = SdkUtil.getLocalVersion();
// 本地sdk文件不存在时,localVersion为-1
if (!SdkUtil.localSdkExists()) {
localVersion = -1;
}
// 从网络中获取最新的sdk version
Pair<Integer, String> remoteSdk = NetUtil.getRemoteSdk();
if (localVersion < remoteSdk.first) {
// 更新
File remoteFile = NetUtil.getFile(remoteSdk.second);
if (remoteFile == null || !remoteFile.exists()) {
return false;
}
// 重置本地version
SdkUtil.setLocalVersion(remoteSdk.first);
return true;
}
return true;
}
}
可以看到,待测试代码即为一个简单的sdk下载更新流程的代码,流程较为简单,下面我们开始分析:
-
确定测试函数:sdk下载更新。对于项目中的实际功能函数: UpdateExample.updateSdk
-
分析函数功能,确定输入输出。
- 功能:检查本地sdk是否存在或者是否需要更新,如需要,则下载网络js sdk文件,并更新本地js sdk版本
- 输入:无
- 输出: boolean值,操作是否成功。
-
确定函数依赖项
确定好依赖项,决定哪些条件需要mock。
- 本地sdk是否存在
- 本地sdk版本号与网返回的sdk版本号的大小关系
- 网络下载sdk文件是否成功
-
分析程序环路复杂度,如下图:
可以看到,边数e = 10,顶点数n = 9,所以程序环路复杂度V(G) = e - n + 2 = 3,即至少需要3个测试case才能比较好的覆盖待测函数
-
建立单元测试case列表
- case1: 本地sdk不存在 -> 会进行网络请求,且使网络请求成功 -> 会返回true,并设置本地sdk版本为网络返回的值
- case2: 本地sdk不存在 -> 会进行网络请求,且使网络请求失败 -> 会返回false
- case3: 本地sdk文件存在,但版本过旧 -> 会进行网络请求,且使网络请求成功 -> 会返回true,并设置本地js sdk版本为网络返回的值
-
编写单元测试代码
这里以第一个case为例
条件:本地sdk文件不存在,且使网络请求返回成功
预期:会进行网络请求sdk文件(通过mock返回一个本地文件),且下载更新最终返回成功。
代码:
@Test public void testWithLocalSdkFileNotExists() { Application application = RuntimeEnvironment.application; //删除本来的sdk,保证本地sdk文件不存在 SdkUtil.deleLocalSdk(); // 将本地assets目录下的jssdk文件copy到指定临时目录(本地js sdk文件版本为2201) File testFile = new File(application.getApplicationContext().getFilesDir(), "tempSdk"); // mock住NetUtils.getRemoteSdk,保证网络请求下发的sdk版本够大 mockNetUtils(application.getApplicationContext(), Integer.MAX_VALUE); // mock住网络下载sdk文件,返回本地的一个sdk临时文件. mockNetManager(testFile, true); //设置localVersion值,使version值小于网络下发的version SdkUtil.setLocalVersion(0); //调用实际的下载更新sdk函数 boolean result = UpdateExample.updateSdk(); // 实际结果验证,预期为true Assert.assertTrue("本地sdk文件不存在,sdk下载更新测试", result); }
具体需要mock和环境的设置都在代码中,且相应步骤有注释,比较简单易懂,不再详述。
如何写
环境配置
JUnit
框架的引入:
testImplementation 'junit:junit:4.12'
Mockito
框架的引入:
testImplementation "org.mockito:mockito-core:${project.ext.unitTests.mockito}"
PowerMock
框架引入(注意Mockito
框架版本要与PowerMock
框架版本兼容):
testImplementation "org.powermock:powermock-module-junit4:${project.ext.unitTests.powermock_module}"
testImplementation "org.powermock:powermock-module-junit4-rule:${project.ext.unitTests.powermock_module_junit4_rule}"
testImplementation "org.powermock:powermock-api-mockito2:${project.ext.unitTests.powermock_module}"
testImplementation "org.powermock:powermock-classloading-xstream:${project.ext.unitTests.powermock_module}"
Robolectric
框架引入:
testImplementation "org.robolectric:robolectric:${project.ext.unitTests.robolectric}"
Robolectric
的一些配置:
testOptions {
unitTests {
includeAndroidResources = true
}
}
gradle.properties
加入配置:
android.enableUnitTestBinaryResources=true
测试类的基本配置: