一文带你解决Android app手工测试或者自动化测试覆盖率统计(撸代码版)

2021-03-15 16:23:40 浏览数 (1)

我们经常会遇到这样的问题。

代码语言:javascript复制
1.手工测试覆盖率是多少?
2.UI自动化覆盖率是多少?
3.你怎么保证你覆盖了全部的场景?

其实这三个问题不难回答,可以从两个维度,

1.覆盖了需求的是多少,用例评审时,就是一个很好的统计。如果不到,会有补充,但是这个人为因素多,可能不全面。

2.看下功能测试或者UI自动化测试对于app 的代码的覆盖度是多少?

要想看到这个,我们必须要用工具呢,有了工具,我们才很好的去度量呢。我们选择Jacoco。那么如何来做呢。接下来,我们一起去解密,如何统计app 代码覆盖率。

首先,我们要在安卓代码中引入我们的依赖。在我们待测app的build.gradle做如下配置,引入我们的jacoco。

代码语言:javascript复制
apply plugin: 'jacoco'
jacoco {
    toolVersion = "0.8.4" #依赖版本号
    description("$buildDir/filescoverage.exec")#覆盖率文件的路径
    reportsDir = file("$buildDir/reports/jacoco")#测试报告路径

}

配置完毕后,Android studio自动去给我们加载包。

接下来就是代码去实现了。我们去创建一个接口,FinishListener。接口主要有两个方法。

代码语言:javascript复制
public interface FinishListener {
    void onActivityFinished();
    void dumpIntermediateCoverage(String filePath);
}

我们新起一个InstrumentedActivity,这个的目的呢,开始收入代码覆盖数据。

代码语言:javascript复制
public class InstrumentedActivity extends LoginActivity {
    public static String TAG = "InstrumentedActivity";

    private FinishListener mListener;

    public void setFinishListener(FinishListener listener) {
        mListener = listener;
    }


    @Override
    public void onDestroy() {
        super.onDestroy();
        super.finish();
        if (mListener != null) {
            mListener.onActivityFinished();
        }
    }
}

那么我们接下来去实现一个JacocoInstrumentation。

代码语言:javascript复制
public class JacocoInstrumentation extends Instrumentation implements
        FinishListener {
    public static String TAG = "JacocoInstrumentation:";
    private static String DEFAULT_COVERAGE_FILE_PATH = "coverage.ec";


    private final Bundle mResults = new Bundle();

    private Intent mIntent;
    private static final boolean LOGD = true;

    private boolean mCoverage = true;

    private String mCoverageFilePath;

    @Override
    public void onCreate(Bundle arguments) {
        Log.d(TAG, "onCreate("   arguments   ")");
        super.onCreate(arguments);

        File file = new File(getContext().getFilesDir(),"coverage.ec");
        System.out.println(file.getAbsolutePath());
        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();
    }

    @Override
    public void onStart() {
        if (LOGD)
            Log.d(TAG, "onStart()");
        super.onStart();

        Looper.prepare();
        InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
        activity.setFinishListener(this);
    }

    private void generateCoverageReport() {
        Log.d(TAG, "generateCoverageReport():"   getCoverageFilePath());
        OutputStream out = null;
        try {
            out = new FileOutputStream(getContext().getFilesDir() 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 (Exception e) {
            Log.d(TAG, e.toString(), e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String getCoverageFilePath() {
        if (mCoverageFilePath == null) {
            return DEFAULT_COVERAGE_FILE_PATH;
        } else {
            return mCoverageFilePath;
        }
    }

    private boolean setCoverageFilePath(String filePath) {
        if (filePath != null && filePath.length() > 0) {
            mCoverageFilePath = filePath;
            return true;
        }
        return false;
    }


    @Override
    public void onActivityFinished() {
        if (LOGD)
            Log.d(TAG, "onActivityFinished()");
        if (mCoverage) {
            generateCoverageReport();
        }
        finish(Activity.RESULT_OK, mResults);
    }

    @Override
    public void dumpIntermediateCoverage(String filePath) {
        // TODO Auto-generated method stub
        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);
        }
    }
}

这里,我们用到的就是统计覆盖的数据,最后生成文件,最后的文件生成是对应activity 销毁。

到这里,我们还需要去配置我们的响应的权限,因为要用到对应的权限。

代码语言:javascript复制
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

我们还需要吧对应的activity加载进来。

代码语言:javascript复制
 <activity android:label="InstrumentationActivity"
            android:name="com.example.studayappp.test.InstrumentedActivity" />

由于是基于instrumentation,我们还需要对instrumentation进行相关的配置。

代码语言:javascript复制
 <instrumentation
            android:handleProfiling="true"
            android:name="com.example.studayappp.test.JacocoInstrumentation"
            android:functionalTest="false"
            android:label="tihis test"
            android:targetPackage="com.example.studayappp">
        </instrumentation>

配置完毕,我们就可以打包,然后用adb 执行下面的命令,去启动app

代码语言:javascript复制
adb shell am instrument 包名/test.JacocoInstrumentation

启动app后,就可以正常测试。最后,我们返回或者杀掉应用。就可以产生对应的文件,路径在下面

代码语言:javascript复制
/data/data/yourPackageName/files/coverage.ec

然后我们去pull下我们的覆盖率文件即可。最后呢,我们利用app的build.gradle配置一个任务即可

代码语言:javascript复制
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: './build/intermediates/javac/debug/classes',
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
            ])
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/filescoverage.exec")

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

如果配置中无法识别task任务中的方法的,可能是因为版本不一样,我的版本如下

这样我们去执行

代码语言:javascript复制
gradlew.bat   jacocoTestReport

就可以产生对应的测试报告了。

如果我们经过手工测试, 出来一个这样的报告,我们就可以告诉我的覆盖率是多少。那么反过来,我们也会发现,原来我们的用例也有覆盖不全的地方,即使我们经过用例的评审的阶段,还会出现覆盖不到的地方。但是我们满足了业务的100%覆盖,还有未覆盖的,我们需要斟酌覆盖的投入产出比。

代码语言:javascript复制
 代码覆盖率100% 不代表没有bug。代码没有覆盖100% 一定有bug

但是有可能你覆盖到80% 很轻松,往后增加5% 都费很大劲。那么我们可以去没有覆盖到的进行分析。不一定要做到代码100%全覆盖,尤其在功能测试阶段,代码100% 覆盖,会给大家增加很多的工作量,很有可能为了1%的覆盖率而耽误整体测试,得不偿失。覆盖率是为了提升我们测试用例的覆盖度,检验我们测试用例设计的全面性,它有两面性,合理引入覆盖率,合理选择一定的阈值。

本文介绍了Jacoco统计安卓app手工测试覆盖率的方法,这里没有做增量代码的覆盖率,没有做多人分工测试app,测试报告如何合并,如何启动不用Instrumentation直接启动app。后续的文章中,将会持续分享。

0 人点赞