在Android Studio上进行单元测试是相对来说比较简单的,主要可以分为两类:
在运行local unit test时,android.jar将不包含任何实际的代码,如果在运行local unit test时调用了android框架代码,那么将会报
Error: "Method... not mocked"
解决方案有两种:
利用instrumentation test来进行测试
在project build.gradle文件中添加
android {
...
testOptions {
unitTests.returnDefaultValues = true
}
}
这样所有的android框架代码的方法均会返回默认值0或者null。当然这并不是一种值得推荐的方法。
Android Studio默认使用JUnit来进行单元测试,但是你也可以Mockito等测试框架来拓展测试能力。
instrumentation test实际上运行了两个app。第一个是被测试的app,第二个是instrumentation test所在的application,androidTest目录下其实包含了一个完整的工程,如manifest文件、资源文件等,只不过这些文件已经完全被隐藏。系统在同一个进程下运行这两个app,因此测试app可以调用app的方法和修改app的变量。
一般来说,这两种单元测试各有其优缺点,我一般用instrumentation test比较多。
Android Studio默认已经给我们添加好了单元测试需要的依赖框架,我们只需要在相应的类或方法上,点击Ctrl+Shift+T生成相应的测试类,然后编写相应的测试代码即可。
for example:
新建一个Calculator类
public class Calculator {
public int add(int a, int b) {
return a = b;
}
}
在Calculator类上点击ctrl+shift+T,直接在androidTest/java/目录下生成CalculatorTest.java文件。然后在CalculatorTest.java右键可以直接运行该单元测试文件。
public class CalculatorTest {
@Test
public void add() throws Exception {
Calculator calculator = new Calculator();
assertEquals(2, calculator.add(1, 1));
}
}
为什么这里选择androidTest/java而不是test/java目录,因为前者是真正的Android设备环境,使用起来也很方便,并且后续生成测试代码覆盖率报告时也会很方便。
有时我们想单独对某个模块进行测试,但是这个模块依赖于其他未完成的模块,这时我们可以使用单元测试的mock功能,单元测试的mock也分为两种:
常用的local unit test单元测试框架为mockito,使用方法分为如下几个步骤:
1、在module的build.gradle文件中添加mockito依赖
testCompile 'org.mockito:mockito-core:1.10.19' // 注意这里testCompile和androidTestCompile的区别,testCompile是对local unit test的,而androidTestCompile针对于instrumentation test
2、在测试的定义处添加注解
@RunWith(MockitoJUnitRunner.class)
3、创建mock对象,并在定义处添加@Mock注解
4、构建when()… then()… 语句定义mock的方法和返回值。
@RunWith(MockitoJUnitRunner.class)
public class CalculatorTest {
@Mock
Calculator calculator;
@Test
public void add() throws Exception {
when(calculator.add(3,2))
.thenReturn(4);
assertEquals(4, calculator.add(1, 2));
}
}
这里有几个值得注意的事:
testCompile ‘org.mockito:mockito-core:1.10.19’,不要写为androidTestCompile ‘org.mockito:mockito-core:1.10.19’,否则会找不到mockito依赖。
如果when…then… 语句中定义的参数和实际调用的不一致,那么如果返回值是基本类型,则返回空,否则返回null。
一般对于网络服务单元测试有两种选择,一种是对网络服务进行mock,另一种是直接访问网络。这里我更倾向于第二种方式,因为首先mock那么多的网络接口也是一种麻烦事,其次mock的接口的返回情况可能和实际值也会存在一定的偏差。
网络服务一般也可以分为两种,第一种为同步服务,另一种为异步服务。同步服务直接调用接口获取返回值并对返回值进行判断即可。而异步稍微有点麻烦。
要想对异步服务进行测试,我们首先要清楚Android Studio对单元测试代码运行的基本原理。Android Studio采用了一个单线程,(除了@Before和@After),不断调用@Test方法(不保证调用的顺序),一定要注意的是assert方法必须要在该线程中执行才会有效。知道了这点,异步服务的单测也就明朗了。
我们在发送请求后,将单元测试线程阻塞住,等到获取服务器返回的结果后,再释放单元测试线程,并在单元测试线程中执行assert语句即可。
@Test
public void createDir() throws Exception {
CreateDirRequest createDirRequest = new CreateDirRequest();
CreateDirResult createDirResult = client.createDir(createDirRequest); // 同步网络服务接口
assertEquals(true, isSuccess(createDirResult)); // 判断网络任务是否正确执行
}
@Test
public void createDirAsyn() throws Exception {
final CountDownLatch countDownLatch = new CountDownLatch(1);
CreateDirRequest createDirRequest = new CreateDirRequest();
unitTestResult = false; // 用于记录执行结果
createDirRequest.setListener(new ICmdTaskListener() {
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
unitTestResult = isSuccess(cosResult);
countDownLatch.countDown();
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
unitTestResult = isSuccess(cosResult)
countDownLatch.countDown();
}
});
client.createDir(createDirRequest); // 执行异步任务
countDownLatch.await(); // 等待任务执行结束
assertEquals(true, unitTestResult); // 判断执行结果
}
1、运行local unit test
gradlew testDebugUnitTest
2、运行instrumentation test并生成测试报告
gradlew createDebugCoverageReport
注意想要生成代码覆盖率报告,需要在module的build.gradle文件下添加
buildTypes {
debug {
testCoverageEnabled true
}
}
1、Context context = new MockContext();
2、Context context = InstrumentationRegistry.getContext(); // 获取测试app的context
3、Context context = InstrumentationRegistry.getTargetContext(); // 获取被测试app的context
第一种方式获取的context在调用getSystemService()方法时或报错,而后两种不会。
我在Android Studio 2.3版本上运行instrumentation test,并在Android Monitor中不选择任何过滤器,是可以查看日志的。但是同事在Android Studio 2.1版本上却无法打印日志。
场景:自己在Android项目下新建了一个module,其中有个方法调用了写文件代码,然后在进行单元测试时总是报:
java.io.FileNotFoundException: /storage/emulated/0/test.txt: open failed: EACCES (Permission denied)
原因:需要在该module上添加权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
其次由于Android6.0的动态权限特性,如果是targetSdkVersion >= 23,且运行单元测试的手机系统大于或者等于Android6.0,需要降低targetSdkVersion或者使用低于Android6.0的手机。
是否添加了如下依赖:
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
是否配置了
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
这个错误导致的原因有很多,基本上都是使用或者配置上的错误。
public class CalculatorTest {
@Mock
Calculator calculator;
@Test
public void add() throws Exception {
//calculator = new Calculator(); // 这里不能再初始化
when(calculator.add(1,2))
.thenReturn(4);
calculator = new Calculator();
assertEquals(4, calculator.add(1, 2));
}
}
在单独运行每个测试类时运行正常,但是调用./gradlew connectedAndroidTest命令来运行单元测试就会报如上错误。更奇葩的是自己在api level为16的模拟器上没有这个问题,在api level为23的真机上就会出现这个问题。
这里值得注意的是instrumentation test并不强制需要application module,只包含library module完全可以进行单元测试
检查了一下午,才发现fastjson和testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner”有冲突,奇葩的是在1.2.35以及之前的版本存在这个问题,1.2.36和1.2.37就会有这个问题。