前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文带你解决Android app手工测试或者自动化测试覆盖率统计(撸代码版)

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

作者头像
雷子
发布2021-03-15 16:23:40
1.3K0
发布2021-03-15 16:23:40
举报

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

代码语言: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。后续的文章中,将会持续分享。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 雷子说测试开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档