Android Jacoco Manual Coverage
Author: LightingContour
GitHub
GitHub went down… But it’s good, now can address: https://github.com/LightingContour/LC-JacocoSample
The introduction
I have done Android coverage these days, using Jacoco, planning to do a manual coverage Demo. Very happy at the beginning, see all kinds of tutorials, very simple appearance. But step by step to do, encountered many problems many pits, a card a day. Well, now we’re finally out of it. Here is the simplest manual coverage Demo tutorial. Write to everyone who comes to this road later, but also to consolidate what they have learned.
Introduction to the
There are three buttons in the main Activity. Each of the first two buttons only changes the contents of the TextView. The third click exports coverage. There’s also a little Easter egg hidden in the first two Button codes for test coverage. Supporting tool version: Android Studio3.2
You need the knowledge of Get
Jacoco profile
JaCoCo coverage is an open source tool (website address: http://www.eclemma.org/JaCoCo/), it aimed at the development of language is Java, its usage is very flexible and can be embedded into the Ant, Maven; As an Eclipse plug-in, you can use its JavaAgent technology to monitor Java programs, and so on.
Many third-party tools provide integration with JaCoCo, such as Sonar, Jenkins, etc.
Instrumentation
Instrumentation is similar to Acitivity, but without a graphical interface. You can think of it as a utility class for monitoring other classes.
Inherited from the following tutorial
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
Design ideas
1. First write a basic Activity with Xml. 2. Add storage permission on the basis of this Activity, we will save the coverage file to the SD card. Adding coverage code
Do it with me
Creating the base program
1. Create a Project named lC-JacocoSample. Select Empty Activity on the boot page.
Note also that Gradle version 3.1.3 is used here. There will be a pit behind ~!
3. Change the MainActivity location. In Android view of com. Lightingcontour. Under new jacocotry Package: app, test, Utils. This is to prepare for later. Utils is used to store permission access files, and test is used to store coverage files. Then drag the MainActivity under the APP Package.
4.XML layout file updates three buttons, very simple configuration
<? 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>
Copy the code
5. Add code to MainActivity, bind Button, change TextView value when clicking Button, etc
Public class MainActivity extends AppCompatActivity implements view. OnClickListener {// Define a component used in 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); 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,"Clicked the first button.",Toast.LENGTH_SHORT).show();
A.setText("Clicked the first button."); // Set egg: click the first button three times, flag istrue
if (AClickedTime < 3)
{
AClickedTime++;
}else {
easterEgg = true;
}
break;
case R.id.Btn2:
Toast.makeText(this, "Click the second button.", Toast.LENGTH_SHORT).show();
B.setText("Click the second button."); // Set the egg to flagtrue", perform the following operationsif (easterEgg == true)
{
A.setText("Congratulations on entering the egg.");
B.setText("Congratulations on entering the egg.");
}
break;
case R.id.Btn3:
Toast.makeText(this,"Click the third button.",Toast.LENGTH_SHORT).show();
break; }}}Copy the code
6. Test it out, build-run. Program run successfully ~ Part 1 complete!
Add an SD card storage permission
Before we can write the coverage code, we need to figure out the SD card storage permissions. We need to put the coverage file into the SD card of the phone first. However, as you know, starting from Android6.0, not only to add permissions in the manifest, but also in the program to dynamically apply for access. 1. Modify the manifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Copy the code
2. Add PermissionUtils for obtaining storage permission
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}; /** * Checksif the app has permission to write to device storage
* If the app does not has permission thenThe user will be prompted to * Grant permissions * * Check whether App has write permission for SD card * * * @param Activity */ public static void verifyStoragePermissions(Activity activity) {// Checkif 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(); }}}Copy the code
3. Call in the onCreate method to add permission when MainActivity starts
Dynamic application / / SD card read permissions PermissionUtils. VerifyStoragePermissions (this);Copy the code
Call the Jacoco
1.1 Added finishListener.java to test Package
package com.lightingcontour.lc_jacocosample.test;
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
Copy the code
1.2 in the test Package in new 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(); }}}Copy the code
1.3 in the test Package in new 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; Private static final Boolean 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,"Abnormal."+ 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);
returntagString ! = null && Boolean.parseBoolean(tagString); } private StringgetCoverageFilePath() {
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 voidUsegenerateCoverageReport() {
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); }}}Copy the code
2. Change Gradle file of App Model 2.1 Added Jacoco
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.6.201602180812"
}
Copy the code
2.2 Adding Jacoco permission in Manifest
<! -- Jacoco permission --> <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" />
Copy the code
2.3 A task is added to convert coverage EC files of applications into HTML-readable documents
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('? '.'$')}}}}Copy the code
3. Add a call to generate Jacoco coverage in MainActivity. 3.1 Add a call to JacocoInstrumentation
import com.lightingcontour.lc_jacocosample.test.JacocoInstrumentation; Public class MainActivity extends AppCompatActivity implements view.onClickListener {// Add the following call to public JacocoInstrumentation jacocoInstrumentation = new JacocoInstrumentation();Copy the code
When Button3 is clicked, the generate coverage method is called
case R.id.Btn3:
Toast.makeText(this,"Click the third button.",Toast.LENGTH_SHORT).show();
jacocoInstrumentation.UsegenerateCoverageReport();
break;
Copy the code
Ready to go! So just to clarify we added three new files to the test Package for Jacoco testing. Permissions have been added in Manifest. A task for generating HTML coverage reports from EC files has been added to Gradle. Added calls to Jacocos Instrumentation in MainActivity and coverage EC files generated when the third Button is clicked. Then it’s off to run! ~
Output coverage file
Gradlew is used for Windows. InstallDebug can also be used with installDebug in Gradle view
adb shell am instrument -w -r com.lightingcontour.lc_jacocosample/.test.JacocoInstrumentation
Copy the code
The real phone or AVD will pop up a finished App, operate, click a few buttons and so on. Finally, click Button3 to export our coverage file.
Click Finish to exit the App, so that the command line will prompt you to exit.
3. Use adb command to copy to our computer. I’m using a MAC here, so I’m copying it directly to the desktop.
adb pull mnt/sdcard/coverage.ec ~/Desktop/123.ec
Copy the code
After the success of the Pull, will get $buildDir) specified in the file into the task/outputs/code – coverage/connected/coverage. In the ec, is the * *… LC – JacocoSample/app/build/outputs/code – coverage/connected * * and then use the Gradle jacocoTestReport or command line in view, will do
Finally, the report generated in… / LC – JacocoSample/app/build/reports/jacoco/jacocoTestReport/HTML
Congratulations, we’re done
After the source will be uploaded to Github, welcome to point a Star! If you have any questions, please make an issue on Github, and I will read it on it.
Pit records encountered
- JacocoTestReport without output – app/build/intermediates/classes without reason: gradle is too new, compile file changes – Project: JacocoTry gradle version to 3.1.3
- Unable to read execution data file… ToolVersion -jacoco {toolVersion = “0.7.6.201602180812”} We can try if encountered at https://blog.csdn.net/roxxo/article/details/77720300#commentBox
The resources
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
https://blog.csdn.net/o279642707/article/details/54576307
https://cloud.tencent.com/developer/article/1038055