51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 585|回复: 1
打印 上一主题 下一主题

[原创] 大神带你如何获得代码覆盖率(上)

[复制链接]
  • TA的每日心情
    无聊
    昨天 09:19
  • 签到天数: 933 天

    连续签到: 5 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2023-1-30 09:59:40 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    代码覆盖率
      代码覆盖率是软件测试中一个重要的评价指标,主要是指程序运行过程中,被执行到的代码在总代码中的占比程度。
      现在有很多插件可以实现这个功能,应用比较广的就是JaCoCo,虽然好久没更新了,类似Jetbrain旗下的IDE。默认提供了三种获得代码覆盖率的方式:Intellij IDEA、JaCoCo、Emma。
      功能测试代码覆盖率
      常规的代码覆盖率通常是在单元测试中,通过编写测试用例,然后执行对应的单元测试,获得代码覆盖率。当然,现在也有挺多自动化生成单元测试的工具,比如EvoSuite,Randoop等。
      但是,对于用户交互性比较强的应用,比如Android应用,这种单纯的单元测试显然是满足不了需要的。功能测试就是在真实的使用环境下,人工或者模拟真人对应用进行测试,在这种场景下,生成的代码覆盖率定义其为功能覆盖率。
      JaCoCo与Gradle获取功能测试代码覆盖率
      常见获取覆盖率的方法分两种,一种是在源码中,以打桩的形式,收集覆盖率数据,针对性较强,但是需要深入源码,另一种是改写应用入口,通过instrument启动应用,记录应用执行期间全局的代码覆盖率。这里,主要针对第二种,主要内容分为两个主要部分:
      覆盖率数据获取
      覆盖率数据解析
      具体实现
      入口文件改写
      这里需要添加一个新的入口,接收instrument指令,启动代码覆盖率记录功能,并启动原始应用的入口Activity。涉及的一些代码,网上到处都是,我找了一些改写了一下,除去了一些冗余的代码,也去掉了一些bug。
      这里主要包括2个java文件,为了方便管理,我们就单独创建一个package、test,避免和原始代码混淆。
      一个Instrument启动器,目的是方便通过instrument指令启动带有coverage记录功能的activity。
    1. package test;

    2. import android.app.Instrumentation;
    3. import android.content.Intent;
    4. import android.os.Bundle;
    5. import android.util.Log;

    6. import java.io.File;
    7. import java.io.IOException;

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

    11.     @Override
    12.     public void onCreate(Bundle bundle) {
    13.         Log.d(TAG, "onCreate(" + bundle + ")");
    14.         super.onCreate(bundle);
    15.         String DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";

    16.         File file = new File(DEFAULT_COVERAGE_FILE_PATH);
    17.         if (!file.exists()) {
    18.             try {
    19.                 file.createNewFile();
    20.             } catch (IOException e) {
    21.                 Log.d(TAG, "异常 : " + e);
    22.                 e.printStackTrace();
    23.             }
    24.         }
    25.         mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
    26.         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    27.         start();//调用onStart
    28.     }

    29.     @Override
    30.     public void onStart() {
    31.         Log.d(TAG, "onStart()");
    32.         super.onStart();
    33.         startActivitySync(mIntent);
    34.     }
    35.     //adb shell am instrument com.tachibana.downloader/test.JacocoInstrumentation
    36. }
    复制代码


    这个文件的主要功能有两点:
      在/data/data/应用包名/files目录下创建coverage.ec,这个文件就是用来记录覆盖率数据的;
      启动改写的Activity,这里是InstrumentedActivity。
      为了不影响原始代码,创建一个原入口类的子类,实现记录覆盖率数据的功能,在实现目的的情况下,尽可能减少对原始代码的影响。
    1. package test;

    2. import android.util.Log;

    3. import com.tachibana.downloader.ui.main.MainActivity;

    4. import java.io.FileOutputStream;
    5. import java.io.OutputStream;


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

    8.     @Override
    9.     public void onDestroy() {
    10.         super.onDestroy();
    11.         Log.d(TAG, "onDestroy()");
    12.         generateCoverageReport();
    13.     }

    14.     private void generateCoverageReport() {
    15.         String DEFAULT_COVERAGE_FILE_PATH = getFilesDir().getPath() + "/coverage.ec";
    16.         Log.d(TAG, "generateCoverageReport():" + DEFAULT_COVERAGE_FILE_PATH);
    17.         try {
    18.             OutputStream out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, false);
    19.             Object agent = Class.forName("org.jacoco.agent.rt.RT")
    20.                     .getMethod("getAgent")
    21.                     .invoke(null);

    22.             out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
    23.                     .invoke(agent, false));
    24.             out.close();
    25.         } catch (Exception e) {
    26.             Log.d(TAG, e.toString(), e);
    27.         }
    28.     }

    29. }
    复制代码


    如代码里展示的,我这里原始的入口类是com.tachibana.downloader.ui.main.MainActivity,我们继承这个Activity,并实现了一个generateCoverageReport方法,用来记录JaCoCo的数据,在这个Activity退出的时候,调用方法记录,实现覆盖率数据获取的功能。
      这个函数也是从网上找的,从字面上来看,逻辑就是每次这个Activity的生命周期执行到onDestroy的时候,就记录一次数据,并且是覆盖式的记录。(对于覆盖式的记录,我是有点困惑的,这些数据是从应用启动就被一直存在内存,增量式增加还是记录在日志,每次调用这个方法的时候,jacoco去日志里面解析暂且认为这种方式没有问题吧,毕竟网上都是这么做的)。
      这个Activity的作用也是2个:
      记录ExecutionData,也就是覆盖率的原始数据;
      Fake入口,继承了原始的入口Activity,在实现原始功能的同时,不影响原来代码。
      AndroidManifest.xml改写
      配置文件的改写主要分为3个部分:
      增加权限,既然读写文件了,那读写文件的权限肯定是要有的:
    1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    复制代码


    Activity索引,我们新建了一个Fake入口,需要将这个Activity加入配置文件中,就加在<application>标签下,和其他的Activity一样。
    1. <activity
    2.   android:name="test.InstrumentedActivity"
    3.   android:label="InstrumentationActivity" />
    复制代码


    配置Instrument,<instrumentation>标签与<application>标签同级,配置了才能通过adb指令执行到,需要自定义的属性有两个。name,就是上面写的那个,targetPackage,就是应用的包名,配置在AndroidManifest.xml里的那个。
    1. <instrumentation
    2.     android:name="test.JacocoInstrumentation"
    3.     android:handleProfiling="true"
    4.     android:label="CoverageInstrumentation"
    5.     android:targetPackage="com.tachibana.downloader" />
    复制代码


    build.gradle改写
      这里的配置主要针对JaCoCo,在应用构建过程,生成JaCoCo必须的一些数据,以及获取ec文件后的转义处理。
    1. apply plugin: 'com.android.application'
    2. apply plugin: 'jacoco'//引用插件

    3. jacoco {//插件版本设置
    4.     toolVersion = "0.8.5"
    5. }
    6. android {
    7.     compileSdkVersion 30
    8.     buildToolsVersion "30.0.1"

    9.     defaultConfig {
    10.         applicationId "com.morn.aaa"
    11.         minSdkVersion 23
    12.         targetSdkVersion 30
    13.         versionCode 1
    14.         versionName "1.0"

    15.         testInstrumentationRunnerArguments clearPackageData: 'true'//instrument设置
    16.     }

    17.     buildTypes {
    18.         debug {
    19.             testCoverageEnabled = true//JaCoCo功能启动
    20.         }
    21.     }
    22. }
    23. //ec文件解析函数
    24. def coverageSourceDirs = [
    25.         '../app/src/main/java'
    26. ]
    27. task jacocoTestReport(type: JacocoReport) {
    28.     group = "Reporting"
    29.     description = "Generate Jacoco coverage reports after running tests."
    30.     reports {
    31.         xml.enabled = true
    32.         html.enabled = true
    33.         csv.enable = true
    34.     }
    35.     classDirectories.from = fileTree(
    36.             dir: './build/intermediates/javac/debug',
    37.             excludes: ['**/R*.class',
    38.                        '**/*$InjectAdapter.class',
    39.                        '**/*$ModuleAdapter.class',
    40.                        '**/*$ViewInjector*.class'
    41.             ])
    42.     sourceDirectories.from = files(coverageSourceDirs)
    43.     executionData.from = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

    44.     doFirst {
    45.         new File("$buildDir/intermediates/javac/").eachFileRecurse { file ->
    46.             if (file.name.contains('

    47. 这是app/build.gradle的一个例子,关键部分我都加了注释。
    48.   得到ec文件之后,需要将Android设备下的ec文件,放置到$buildDir/outputs/code-coverage/connected/coverage.ec然后运行jacocoTestReport这个task,运行成功后会在$buildDir/reports/jacoco目录下生成根据eoverage.ec转义的html等文件,html目录下的index.html可以可视化展示代码覆盖率数据。
    49. [align=center][color=rgb(68, 68, 68)][url=http://www.51testing.com/batch.download.php?aid=143392][img]http://www.51testing.com/attachments/2023/01/15326825_2023012915333113zA0.png[/img][/url][/color][/align]



    50. [/size][/font]

    51. )) {
    52.                 file.renameTo(file.path.replace('

    53. 这是app/build.gradle的一个例子,关键部分我都加了注释。
    54.   得到ec文件之后,需要将Android设备下的ec文件,放置到$buildDir/outputs/code-coverage/connected/coverage.ec然后运行jacocoTestReport这个task,运行成功后会在$buildDir/reports/jacoco目录下生成根据eoverage.ec转义的html等文件,html目录下的index.html可以可视化展示代码覆盖率数据。
    55. [align=center][color=rgb(68, 68, 68)][url=http://www.51testing.com/batch.download.php?aid=143392][img]http://www.51testing.com/attachments/2023/01/15326825_2023012915333113zA0.png[/img][/url][/color][/align]



    56. [/size][/font]

    57. , '

    58. 这是app/build.gradle的一个例子,关键部分我都加了注释。
    59.   得到ec文件之后,需要将Android设备下的ec文件,放置到$buildDir/outputs/code-coverage/connected/coverage.ec然后运行jacocoTestReport这个task,运行成功后会在$buildDir/reports/jacoco目录下生成根据eoverage.ec转义的html等文件,html目录下的index.html可以可视化展示代码覆盖率数据。
    60. [align=center][color=rgb(68, 68, 68)][url=http://www.51testing.com/batch.download.php?aid=143392][img]http://www.51testing.com/attachments/2023/01/15326825_2023012915333113zA0.png[/img][/url][/color][/align]



    61. [/size][/font]

    62. ))
    63.             }
    64.         }
    65.     }
    66. }


    67. dependencies {
    68.     implementation fileTree(dir: "libs", include: ["*.jar"])
    69. }
    复制代码


    这是app/build.gradle的一个例子,关键部分我都加了注释。
      得到ec文件之后,需要将Android设备下的ec文件,放置到$buildDir/outputs/code-coverage/connected/coverage.ec然后运行jacocoTestReport这个task,运行成功后会在$buildDir/reports/jacoco目录下生成根据eoverage.ec转义的html等文件,html目录下的index.html可以可视化展示代码覆盖率数据。






    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-4-20 08:09 , Processed in 0.065214 second(s), 22 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表