Android代码覆盖率jacoco的傻瓜式demo实现

前言:
Android项目只能使用JaCoCo的离线插桩方式。

为什么?主要是因为Android覆盖率的特殊性:

一般运行在服务器java程序的插桩可以在加载class文件进行,运用java
Agent的机制,可以理解成”实时插桩”。JaCoCo提供了自己的Agent,完成插桩的同时,还提供了丰富的dump输出机制,如File,Tcp
Server,Tcp Client。覆盖率信息可以通过文件或是Tcp的形式输出。这样外部程序可很方便随时拿到被测程序的覆盖率。

但是Android系统破坏了JaCoCo这种便利性,原因有两个:

(1) Android虚拟机不同与服务器上的JVM,它所支持的字节码必须经过处理支持Android Dalvik等专用虚拟机,所以插桩必须在处理之前完成,即离线插桩模式。
(2) Android虚拟机没有配置JVM 配置项的机制,所以应用启动时没有机会直接配置dump输出方式。

一、项目结构

demo项目结构如下:
项目结构

二、详细代码

InstrumentedActivity.java:

package com.example.jacocodemo.test;

import android.util.Log;
import com.example.jacocodemo.MainActivity;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class InstrumentedActivity extends MainActivity {
    public static String TAG = "InstrumentedActivity";

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy()");
        generateCoverageReport();
    }

    private void generateCoverageReport() {
        String DEFAULT_COVERAGE_FILE_PATH = getFilesDir().getPath() + "/coverage.ec";
        Log.d(TAG, "generateCoverageReport():" + DEFAULT_COVERAGE_FILE_PATH);
        try {
            OutputStream out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, 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));
            out.close();
        } catch (Exception e) {
            Log.d(TAG, e.toString(), e);
        }
    }

}

JacocoInstrumentation.java:

package com.example.jacocodemo.test;

import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import java.io.File;
import java.io.IOException;

public class JacocoInstrumentation extends Instrumentation {
    public static String TAG = "JacocoInstrumentation:";
    private Intent mIntent;

    @Override
    public void onCreate(Bundle bundle) {
        Log.d(TAG, "onCreate(" + bundle + ")");
        super.onCreate(bundle);
        String 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();
            }
        }
        mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
        mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        start();//调用onStart
    }

    @Override
    public void onStart() {
        Log.d(TAG, "onStart()");
        super.onStart();
        startActivitySync(mIntent);
    }
    //adb shell am instrument com.tachibana.downloader/test.JacocoInstrumentation
}

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jacocodemo">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.JacocoDemo">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".test.InstrumentedActivity"
            android:label="InstrumentationActivity" />
    </application>
    <instrumentation
        android:name=".test.JacocoInstrumentation"
        android:handleProfiling="true"
        android:label="CoverageInstrumentation"
        android:targetPackage="com.example.jacocodemo" />

</manifest>

build.gradle改写:

apply plugin: 'com.android.application'
apply plugin: 'jacoco'//引用插件

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.jacocodemo"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

//        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArguments clearPackageData: 'true'//instrument设置
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            testCoverageEnabled = true//JaCoCo功能启动
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

//ec文件解析函数
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.from = fileTree(
            dir: './build/intermediates/javac/debug',
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
            ])
    sourceDirectories.from = files(coverageSourceDirs)
    executionData.from = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

    doFirst {
        new File("$buildDir/intermediates/javac/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation fileTree(dir: "libs", include: ["*.jar"])

}

三、操作步骤

  1. 安装debug包到设备上
  2. 退出app应用
  3. cmd命令:adb shell am instrument com.example.jacocodemo/com.example.jacocodemo.test.JacocoInstrumentation
  4. 手动测试
  5. 返回键回到设备主页面
  6. 在设备的文件管理中取回代码覆盖率文件coverage.ec(路径:data/data/包名/files
  7. 复制到项目的 build\outputs\code-coverage\connected
  8. 回到Android Studio,点击右侧的Gradle -> Tasks -> reporting -> jacocoTestReport
  9. 生成的报告(index.html)路径为:build/reports/jacoco/jacocoTestReport/html/com.example.jacocodemo

四、总结

测试覆盖率报告:(从右往左)

指标内容
类覆盖率\
方法覆盖率\
行覆盖率\
圈复杂度覆盖率度量方法里所有可能的最小路径数是否被测试
逻辑分支覆盖率度量方法里if和switch语句分支覆盖情况
指令覆盖率度量单个java二进制代码指令的覆盖情况

拓展:接入Jenkins;增量/全量

参考资料:
https://www.jianshu.com/p/ab2be01d7347#comment-60999311
https://blog.csdn.net/itfootball/article/details/45618609

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值