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

  1. JacocoTestReport without output – app/build/intermediates/classes without reason: gradle is too new, compile file changes – Project: JacocoTry gradle version to 3.1.3
  2. 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