在开始写单测之前,已经调研了很久Android单测的框架以及demo,正好换了一个新的项目组,就从头开始将单测框架应用到开发过程中,后续也方便可以使用TDD。
调研的框架:junit,mockito, roboletric,espresso,jacoco(覆盖率报告)
具体场景:网络请求,todomvp的单测方式,UI测试等。
理想永远是美好的,撸起袖子开始干的时候,就会发现还有很多崎岖需要去踏平。
首先,我放弃了espresso(虽然是google官方出的测试框架),主要原因是espresso依赖真机或模拟器。我相信大部分应用应该都有模拟器判断,在模拟器环境下禁止启动app,为了写单测,还得改代码,很难受;另外多数开发团队应该都是持续集成,跑espresso,还需要有一台手机常年插在构建机上,很麻烦。这并不是说espresso毫无用处,espresso对于Android框架支持非常完善,基本上所有UI的测试都可以实现。另外,espresso还有一个非常强大的功能,适合测试人员手工测试:录制测试过程,自动生成测试代码。比方说:你进入app后,按照正确测试流程点击一遍后,保存下来,下一次开发人员有了些许改动,你只需要运行一次之前自动生成的测试代码,应用便会按照之前的流程进行测试,如出现异常,则测试不通过!非常强大(据团队的IOS工程师说,ios也有一样的测试框架,个人觉得测试人员可以考虑一下这个)。
所以最初我采用的Android单测框架就是:junit + mockito + roboletric + jacoco。
junit和mockito就不要多讲了,都是java单元测试最基本的框架。在说roboletric之前,得先说一下powerMock,mockito无法mock static方法,而powermock则解决了这个问题。所以powermock和mockito配合使用,基本就可以覆盖绝大部分情况。不过由于powermock和mockito是两个团队实现的,经常出现版本不兼容的情况,建议直接使用powermock内部引用的mockito,这样就不会冲突了。
roboletric简单来说就是实现了一套JVM能运行Android代码的框架,从而做到脱离Android环境进行测试。powermock和roboletric在版本上有些不太兼容,roboletric的github的wiki上有官方出的powermock和roboletric的集成方式:
贴上地址:
https://github.com/robolectric/robolectric/wiki/Using-PowerMock
在使用框架时,注意对应版本。页面中的第一句话:
NOTE: PowerMock integration is broken in Robolectric 3.1 and 3.2, but fixed in 3.3.
我使用的完整配置如下:
apply plugin: 'com.android.library'
apply plugin: 'android-apt'
apply plugin: 'com.jakewharton.butterknife'
apply plugin: 'jacoco'
android {
buildTypes {
debug {
testCoverageEnabled true
}
}
}
/**
* 生成jacoco测试报告
*/
task jacocoTestReport(type:JacocoReport, dependsOn: "testDebugUnitTest") {
println("=========jacocoTestReport start");
group = "Reporting"
description = "Generate Jacoco coverage reports"
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/debug",
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
println("path==========>>" + "${project.buildDir}/intermediates/classes/debug")
def coverageSourceDirs = "${project.projectDir}/src/main/java"
println("coverageSourceDirs==========>>" + coverageSourceDirs)
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
println("executionData==========>>" + "$buildDir/jacoco/testDebugUnitTest.exec")
executionData = files("$buildDir/jacoco/testDebugUnitTest.exec")
reports {
xml.enabled = true
html.enabled = true
}
}
jacoco {
toolVersion = "0.7.1.201405082137"
}
dependencies {
compile files('libs/fastjson-1.1.51.android.jar')
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.cmbchina.ccd.pluto:CMBCore:1.0.0-SNAPSHOT@aar'
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.6.1'
testCompile 'org.powermock:powermock-api-mockito:1.6.6'
testCompile 'org.powermock:powermock-module-junit4:1.6.6'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.6'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.6'
}
原本使用powermock的版本是1.7.X,发现使用的过程中各种报错,还是使用了官方的1.6.6版本,不知道这两个团队什么时候兼容能做的很完善。
附上使用BaseTest:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class , sdk = 21)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
@PrepareForTest({CPSNetUtils.class, Common.class})
public abstract class CPSBaseTest {
@Rule
public PowerMockRule rule = new PowerMockRule();
@Before
public void setUp() {
// 将log日志打印到控制台
ShadowLog.stream = System.out;
initLog();
//mockito 初始化
MockitoAnnotations.initMocks(this);
//mock静态方法所在类
PowerMockito.mockStatic(CPSNetUtils.class, Common.class);
Common.application = getApplication();
new FoundationBuildConfig().init();
initNetMock();
mockCommon();
}
}
@Config(constants = BuildConfig.class , sdk = 21):原本想使用23的sdk版本,会有不兼容问题。
@PowerMockIgnore({“org.mockito.“, “org.robolectric.“, “android.“, “org.json.“, “sun.security.“, “javax.net.“}):根据我的理解:powermock类加载器忽略以上类的加载。
@PrepareForTest({CPSNetUtils.class, Common.class}):想mock static的方法,必须加上此注解。
基本配置如上,若上述配置都没问题了,就可以真正开始写单测了。目前Android端的框架使用的是google推荐的mvp框架,优缺点,网上有很多文章,就不在赘述。github地址如下:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/
不可否认的一点,使用mvp框架后单测实现会简单很多,极大程度减少了对view的测试。贴上一段业务测试代码:
public class FeedbackPresenterTest extends CPSBaseTest {
@InjectMocks
private FeedbackPresenter mPresenter;
@Mock
private FeedbackContract.View mView;
@Before
public void setUp() {
super.setUp();
mPresenter = new FeedbackPresenter();
mPresenter.attachView(mView);
}
@Test
public void feedbackFailTest() {
when(mView.getFeedback()).thenReturn("");
when(mView.getContact()).thenReturn("15012341234");
mPresenter.uploadFeedback();
verify(mView).showToastInBottom("反馈信息不能为空!");
}
@Test
@Config(shadows = {ShadowCommon.class})
public void feedbackSuccessTest() {
when(mView.getFeedback()).thenReturn("闪退!");
when(mView.getContact()).thenReturn("15012341234");
mPresenter.uploadFeedback();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
verify(mView).showToastInBottom("操作成功");
}
}
细心的读者会发现在feedbackSuccessTest测试方法中,我开了个线程睡了半秒钟,这时候就要说到网络请求的单元测试了。网络交互是客户端最频繁的场景,而网络的不稳定,会导致客户端出现很多难以预知的情况,崩溃,闪退都有可能发生。所以对于网络请求的单测是重中之重。我使用的网络库是okhttp,而okhttp有一个很强大的功能:Interceptor。interceptor可以拦截网络请求,可以处理完后继续发送,也可以直接直接返回。废话不多说,上代码:
public class MockInterceptor implements CMBHttpInterceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response;
String json = "";
if (chain.request().url().toString().equals(FoundationHostConst.LOGIN)) {
// login
json = "{\"respMsg\": \"用户名或密码错误[CD1105]\",\"respCode\": \"1001\"}";
} else if (chain.request().url().toString().equals(FoundationHostConst.REGISTER)) {
//register
json = "{\n" +
" \"data\": {\n" +
" \"sessionId\": \"c742f1a3915a445d997735413ca12a78\",\n" +
" \"userId\": \"7ac3960080e94be38c79ac83808b579a\",\n" +
" \"channel\": \"MOB\"\n" +
" },\n" +
" \"respMsg\": \"操作成功\",\n" +
" \"respCode\": \"1000\"\n" +
" }";
} else if (chain.request().url().toString().equals(FoundationHostConst.FEEDBACK)) {
//feedback
json = "{\n" +
" \"respMsg\": \"操作成功\",\n" +
" \"respCode\": \"1000\"\n" +
" }";
}
response = setResponse(chain, json);
return response;
}
/**
* 设置指定返回报文
*
* @param chain
* @param response
* @return
*/
private Response setResponse(Chain chain, String response) {
return new Response.Builder()
.code(200)
.addHeader("Content-Type", "multipart/form-data")
.body(ResponseBody.create(MediaType.parse("multipart/form-data"), response))
.message(response)
.request(chain.request())
.protocol(Protocol.HTTP_2)
.build();
}
}
当然也可以模拟异常返回,404什么的都可以。另外okhttp使用的是建造者模式,客户端网络请求OkHttpClient都是一致的,故可以使用类似代码直接mock返回:
CMBHttpClient.Builder builder CMBHttpUtils.getDefaultClientBuilder().addInterceptor(new CPSInterceptor())
.addInterceptor(new CMBLogInterceptor())
.addInterceptor(new MockInterceptor());
PowerMockito.when(CPSNetUtils.getCPSDefaultBuilder()).thenReturn(builder);
单测写完,总得看到点数据报告吧。这时候就需要覆盖率报告了。Android studio自带了jacoco 插件生成覆盖率报告。
jacoco数据含义:
行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
类覆盖率:度量计算class类文件是否被执行。
分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的分支数量。
方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。
参考自:http://blog.csdn.net/tmq1225/article/details/52221187
jacoco的覆盖率报告分为两种:
1. 只生成java层代码覆盖率报告
2. 在运行app期间执行的覆盖率报告
方法也不尽相同。生成java层代码的任务在前面代码中已经贴出。
生成运行app期间执行的覆盖率报告代码如下:
//task jacocoAndroidTestReport(type:JacocoReport,dependsOn:"connectedAndroidTest"){
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports{
xml.enabled = true
html.enabled = true
csv.enabled = false
}
classDirectories = fileTree(
dir : "$buildDir/intermediates/classes/debug",
excludes : [
'**/*Test.class',
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*'
]
)
def coverageSourceDirs = ['src/main/java']
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
additionalClassDirs = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
}
以上代码并未测试过,点击执行测试后,会启动app,并在会生成.ec文件,通过解析.ec文件, 可以生成覆盖率报告。因为前面放弃esspreso时就说过放弃真机测试,所以此方法也未使用。
注意事项:testCoverageEnabled 得设置成true,否则无法生成覆盖率报告。
buildTypes {
debug {
testCoverageEnabled true
}
}
另外,中间遇到一个大坑,被坑了很久。就是jacoco和roboletric也有版本不兼容的问题。。。使用最新的jacoco的版本,发现生成的覆盖率报告始终为0
在stackoverflow中找了查了很久,最后在一个帖子里看到,在jacoco 0.7.3以上的版本使用roboletric就始终为0,尝试了多个版本,发现 0.7.1.201405082137 是OK的。千万不要随便升级版本,否则会出现异想不到的问题。。
好了,下面就看一下覆盖率报告:
后续会接入jenkins,等踩完坑,再补一篇文章。