标签:Android Jacoco 手工覆盖率
作者:LightingContour
之前发布在掘金,现在也在简书上更新下~
GitHub
刚好遇到GitHub宕机……
不过还好,现在可以了
地址:https://github.com/LightingContour/LC-JacocoSample
引言
笔者这几天搞了下Android覆盖率,使用的Jacoco,打算做个手工覆盖率的Demo。
一开始很开心啊,看到有各种教程,很简单的样子(误)。但是一步步做下去,遇到了很多问题很多坑,一卡卡一天。
现在总算折腾出来了。这里做一个最简洁的手工覆盖率Demo教程。写给之后来这条路的大家,也是巩固下自己学到的东西。
简介
本篇主要介绍如何写出通过Jacoco实现的覆盖率Demo
Demo主Activity中有三个Button,前两个分别都只是更改TextView内容。第三个点击会导出覆盖率。
另外为测试覆盖率,还写了个小小的彩蛋隐藏在前两个Button代码中。
配套工具版本:Android Studio3.2
需要Get的知识
Jacoco简介
JaCoCo是一个开源的覆盖率工具(官网地址:http://www.eclemma.org/JaCoCo/),它针对的开发语言是java,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。
很多第三方的工具提供了对JaCoCo的集成,如sonar、Jenkins等。
Instrumentation
Instrumentation和Acitivity很类似,但是没有图形界面。
可以把它理解为用于监控其他类的工具类。
继承自以下教程
https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159
设计思路
1.先写一个最基本的Activity配Xml
2.在这个Activity的基础上添加存储权限,我们会将覆盖率文件保存到SD卡上
3.添加覆盖率代码
跟我一起动手做
创建基础程序
1.创建一个Project,名为LC-JacocoSample。在引导页面选择Empty Activity。
2.Gradle Sync老是转圈圈?请在Project的build.gradle中新增阿里云国内maven地址。然后,Sync一下,所有依赖就会很快Down下来啦。
另外需要注意,这里请使用3.1.3版本的Gradle。后面会有坑~!
3.更改MainActivity位置。在Android视图的com.lightingcontour.jacocotry下新增以下Package:app、test、Utils。
这是为了之后做准备。Utils用来存权限获取相关文件,test用来存覆盖率文件。然后将MainActivity拖到app package下。
4.XML布局文件更新
三个Button,很简单的配置
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".app.MainActivity">
<Button
android:id="@+id/Btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="24dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Test1" />
<TextView
android:id="@+id/Test1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.126"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.186" />
<TextView
android:id="@+id/Test2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="TEST2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.737"
app:layout_constraintStart_toEndOf="@+id/Test1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.176" />
<Button
android:id="@+id/Btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:text="Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Test2" />
<Button
android:id="@+id/Btn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
5.MainActivity中新增代码,绑定Button,点击Button时会更改TextView的值等
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//定义layout中所用组件
public TextView A,B;
private int AClickedTime = 0;
private boolean easterEgg = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//赋值、绑定layout组件
A = (TextView) findViewById(R.id.Test1);
B = (TextView) findViewById(R.id.Test2);
findViewById(R.id.Btn1).setOnClickListener(this);
findViewById(R.id.Btn2).setOnClickListener(this);
findViewById(R.id.Btn3).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId())
{
case R.id.Btn1:
Toast.makeText(this,"点击了第一个按钮",Toast.LENGTH_SHORT).show();
A.setText("点击了第一个按钮");
//设定彩蛋:点击了第一个按钮三次,flag为true
if (AClickedTime < 3)
{
AClickedTime++;
}else {
easterEgg = true;
}
break;
case R.id.Btn2:
Toast.makeText(this, "点击了第二个按钮", Toast.LENGTH_SHORT).show();
B.setText("点击了第二个按钮");
//设定彩蛋:flag为true时,执行以下操作
if (easterEgg == true)
{
A.setText("恭喜进入彩蛋");
B.setText("恭喜进入彩蛋");
}
break;
case R.id.Btn3:
Toast.makeText(this,"点击了第三个按钮",Toast.LENGTH_SHORT).show();
break;
}
}
}
6.测试一下,Build-Run。程序运行成功~第一部分完成!
添加SD卡存储权限
在进行覆盖率代码编写之前,我们还需要先搞定SD卡存储权限。
我们要先将覆盖率文件放到手机的SD卡中。然而大家知道,从Android6.0开始,不仅仅要在manifest中添加权限,还要在程序中去动态申请获取。那么开始吧~
1.修改manifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
2.新增PermissionUtils用于获取存储权限
public class PermissionUtils {
// Storage Permissions 存储权限
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
/**
* Checks if the app has permission to write to device storage
* If the app does not has permission then the user will be prompted to
* grant permissions
*
* 检查App是否有SD卡的写入权限
* 如果没有,让系统提醒授予
*
* @param activity
*/
public static void verifyStoragePermissions(Activity activity) {
// Check if we have write permission
try {
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
3.在MainActivity启动时添加权限获取
onCreate方法中调用
//动态申请SD卡读取权限
PermissionUtils.verifyStoragePermissions(this);
调用Jacoco
1.1在test Package中新增FinishListener.java
package com.lightingcontour.lc_jacocosample.test;
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
1.2在test Package中新增InstrumentationActivity.java
package com.lightingcontour.lc_jacocosample.test;
import android.util.Log;
import com.lightingcontour.lc_jacocosample.app.MainActivity;
public class InstrumentedActivity extends MainActivity {
public static String TAG = "InstrumentedActivity";
private FinishListener mListener;
public void setFinishListener(FinishListener listener) {
mListener = listener;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG + ".InstrumentedActivity", "onDestroy()");
super.finish();
if (mListener != null) {
mListener.onActivityFinished();
}
}
}
1.3在test Package中新增JacocoInstrumentation.java
public class JacocoInstrumentation extends Instrumentation implements FinishListener{
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
private final Bundle mResults = new Bundle();
private Intent mIntent;
//LOGD 调试用布尔
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
public JacocoInstrumentation(){
}
@Override
public void onCreate(Bundle arguments) {
Log.d(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
//DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (!file.exists()) {
try {
file.createNewFile();
}catch (IOException e) {
Log.d(TAG, "异常 :" + e);
e.printStackTrace();
}
}
if (arguments != null) {
mCoverageFilePath = arguments.getString("coverageFile");
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
public void onStart() {
if (LOGD)
Log.d(TAG,"onStart()");
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}
private boolean getBooleanArgument(Bundle arguments, String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);
}
private String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
}else {
return mCoverageFilePath;
}
}
private void generateCoverageReport() {
Log.d(TAG, "generateCoverageReport():" + getCoverageFilePath());
OutputStream out = null;
try {
out = new FileOutputStream(getCoverageFilePath(),false);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData",boolean.class)
.invoke(agent,false));
} catch (FileNotFoundException e) {
Log.d(TAG, e.toString(), e);
} catch (IOException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void UsegenerateCoverageReport() {
generateCoverageReport();
}
private boolean setCoverageFilePath(String filePath){
if (filePath != null && filePath.length() > 0) {
mCoverageFilePath = filePath;
}
return false;
}
private void reportEmmaError(Exception e) {
reportEmmaError(e);
}
private void reportEmmaError(String hint, Exception e) {
String msg = "Failed to generate emma coverage. " +hint;
Log.e(TAG, msg, e);
mResults.putString(Instrumentation.REPORT_KEY_IDENTIFIER,"\nError: " + msg);
}
@Override
public void onActivityFinished() {
if (LOGD) {
Log.d(TAG,"onActivityFinished()");
}
finish(Activity.RESULT_OK,mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath) {
if (LOGD) {
Log.d(TAG,"Intermidate Dump Called with file name :" + filePath);
}
if (mCoverage){
if (!setCoverageFilePath(filePath)) {
if (LOGD) {
Log.d(TAG,"Unable to set the given file path :" +filePath + "as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
2.更改App Model的Gradle文件
2.1新增使用Jacoco
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.6.201602180812"
}
2.2在Manifest中新增Jacoco权限
<!-- Jacoco权限-->
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
2.3新增task,用于将应用跑出来的覆盖率ec文件转换为html可读文档
def coverageSourceDirs = [
'../app/src/main/java'
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: '../app/build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
doFirst {
new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
3.在MainActivity中增加调用生成Jacoco覆盖率
3.1新增调用JacocoInstrumentation
import com.lightingcontour.lc_jacocosample.test.JacocoInstrumentation;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//新增下面这个调用
public JacocoInstrumentation jacocoInstrumentation = new JacocoInstrumentation();
点击Button3的时候,就调用生成覆盖率方法
case R.id.Btn3:
Toast.makeText(this,"点击了第三个按钮",Toast.LENGTH_SHORT).show();
jacocoInstrumentation.UsegenerateCoverageReport();
break;
准备完成!
那么梳理一下
我们在test Package中新增了三个文件用于Jacoco测试。
在Manifest中新增了权限。
在Gradle中新增了用于将ec文件生成html覆盖率报告的task。
在MainActivity中新增了对JacocoInstrumentation的调用以及点击第三个Button时生成覆盖率ec文件。
接下来就是开跑了!~
输出覆盖率文件
1./gradlew(windows用gradlew) installDebug 也可以用gradle视图中的installDebug
记得连接真机或者AVD哈
2.需要adb,没有的装一下
命令行中输入
adb shell am instrument -w -r com.lightingcontour.lc_jacocosample/.test.JacocoInstrumentation
真机或者AVD会弹出做好的App,操作操作,点击下几个按钮之类的。
最后点击下Button3,就会导出我们的覆盖率文件了。
点击完成后就可以退出App了,这样后命令行中也会提示退出。
3.使用adb命令,复制到我们的电脑中。
我这儿用的是mac,直接复制到桌面上。
adb pull mnt/sdcard/coverage.ec ~/Desktop/123.ec
Pull成功后,将得到的文件放到task中指定的$buildDir/outputs/code-coverage/connected/coverage.ec中
也就是.../LC-JacocoSample/app/build/outputs/code-coverage/connected
然后使用Gradle视图中的jacocoTestReport或者命令行,都行
最后,生成的报告在.../LC-JacocoSample/app/build/reports/jacoco/jacocoTestReport/html里
恭喜大家,完成啦~
之后会源码上传到Github,欢迎来点个Star!
有什么问题可以尽量在github中提issue,我会在上面看。
遇到的坑记录
- jacocoTestReport无输出-app/build/intermediates/classes无内容
原因:gradle太新了,编译文件变更-Project:JacocoTry用gradle版本改成3.1.3 - Unable to read execution data file …/coverage.ec
解决方案:改toolVersion-jacoco {toolVersion = "0.7.6.201602180812"}
在其他帖子上也看到改到其他版本的……大家如果遇到了可以尝试下
https://blog.csdn.net/roxxo/article/details/77720300#commentBox
参考资料
https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159