Android Gradle Jacoco:用于集成测试的离线工具

Android Gradle Jacoco: offline instrumentation for integration tests

我们正在构建一个 Android 应用程序,该应用程序已使用 Appium 进行测试。现在我想看看我们的 Appium 测试的测试覆盖率。 我认为这是可能的,因为 Jacoco 支持离线检测 (http://www.eclemma.org/jacoco/trunk/doc/offline.html)。

甚至 jacoco gradle 插件的文档也说:

While all tasks of type Test are automatically enhanced to provide coverage information when the java plugin has been applied, any task that implements JavaForkOptions can be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.

https://docs.gradle.org/current/userguide/jacoco_plugin.html

但是我如何编写 build.gradle 以便在执行 Appium 测试甚至执行手动测试用例时检测我们的验收调试风格并将 exec 文件写入智能手机? 因为这样我就可以提取 exec 文件并将其发送到 SonarQube 进行进一步分析。

谢谢 本

我终于设法让它工作了,我想与您分享解决方案:

为您的 buildType 启用检测并相应地配置 SonarQube 例如

...
apply plugin: 'jacoco'
...

android {
    ...
    productFlavors {
        acceptance {
            applicationId packageName + ".acceptance"
            buildTypes {
                debug {
                    testCoverageEnabled true
                }
            }
        }
    }
}


sonarRunner {
    sonarProperties {
        property "sonar.host.url", "..."
        property "sonar.jdbc.url", sonarDatabaseUrl
        property "sonar.jdbc.driverClassName", sonarDatabaseDriverClassName
        property "sonar.jdbc.username", sonarDatabaseUsername
        property "sonar.jdbc.password", sonarDatabasePassword

        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.sources", "src/main"
        property "sonar.tests", "src/test"
        property "sonar.inclusions", "**/*.java,**/*.xml"
        property "sonar.import_unknown_files", "true"
        property "sonar.java.binaries", "build/intermediates/classes/acceptance/debug"
        property "sonar.junit.reportsPath", "build/test-results/acceptanceDebug"
        property "sonar.android.lint.report", "build/outputs/lint-results.xml"
        property "sonar.java.coveragePlugin", "jacoco"
        property "sonar.jacoco.reportPath", "build/jacoco/testAcceptanceDebugUnitTest.exec"
        // see steps below on how to get that file:
        property "sonar.jacoco.itReportPath", "build/jacoco/jacoco-it.exec"

        property "sonar.projectKey", projectKey
        property "sonar.projectName", projectName
        property "sonar.projectVersion", appVersionName
    }
}

将以下内容添加到您的 AndroidManifest.xml

<receiver
 android:name=".util.CoverageDataDumper"
 tools:ignore="ExportedReceiver">
 <intent-filter>
    <action android:name="org.example.DUMP_COVERAGE_DATA"/>
 </intent-filter>
</receiver>

CoverageDataDumper 应如下所示:

public class CoverageDataDumper extends BroadcastReceiver {
   private static final Logger LOG = LoggerFactory.getLogger( CoverageDataDumper.class );

   @Override
   public void onReceive( Context context, Intent intent ) {
      try {
         Class
            .forName( "com.vladium.emma.rt.RT" )
            .getMethod( "dumpCoverageData", File.class, boolean.class, boolean.class )
            .invoke( null,
               new File( App.getContext().getExternalFilesDir( null ) + "/coverage.ec" ),
               true, // merge
               false // stopDataCollection
            );
      }
      catch ( Exception e ) {
         LOG.error( "Error when writing coverage data", e );
      }
   }
}

然后 运行 您的 Appium 测试用例与验收风格应用程序(使用检测 类)。在调用 "Reset App" 或 "Close Application" 之前,请确保调用以下方法(只是一个草稿,但我想你明白了):

// intent is "org.example.DUMP_COVERAGE_DATA"
public void endTestCoverage( String intent ) {
  if ( driver instanceof AndroidDriver ) {
     ((AndroidDriver) driver).endTestCoverage( intent, "" );
  }
}
public void pullCoverageData( String outputPath ) {
  String coverageFilePath = (String) appiumDriver.getCapabilities().getCapability( "coverageFilePath" );
  if ( coverageFilePath != null ) {
     byte[] log = appiumDriver.pullFile( coverageFilePath );
     MobileAppLog.writeLog( new File( outputPath ), log );
  }
  else {
     throw new AppiumLibraryNonFatalException(
        "Tried to pull the coverage data, but the coverageFilePath wasn't specified." );
  }
}

outputPath 可以是例如:/sdcard/Android/data/org.example.acceptance/files/coverage.ec

现在 Jacoco 数据已写入智能手机。接下来我们需要下载那个文件。您可以使用

appiumDriver.pullFile( logFilePath );

现在您需要将文件 "jacoco-it.exec" 复制到 build/jacoco/jacoco-it.exec 中,请参阅 gradle.build 以上和 运行

gradlew sonarRunner

在 SonarQube 中添加 Integration Test Coverage 小部件,您现在应该会看到一些值...

不幸的是,如果您使用 retrolambda(就像我们一样),代码覆盖将不起作用。 Retrolambda 将生成不属于源文件的匿名 类 - 因此 SonarQube 无法正确匹配它们并显示比实际低得多的代码覆盖率。如果有人为此找到解决方案,我会很高兴:-)

我通过向您测试的应用程序添加广播接收器解决了这个问题! (您只能将接收器添加到调试文件夹,因为主源中不需要它)

 public class CoverageReceiver extends BroadcastReceiver {
    private static final String EXEC_FILE_PATH = "/mnt/sdcard/coverage.exec";
    private static final String TAG = "CoverageJacoco";
    private static final String BROADCAST_RECEIVED_MESSAGE = "EndJacocoBroadcast broadcast received!";
    private static final String EMMA_CLASS = "com.vladium.emma.rt.RT";
    private static final String EMMA_DUMP_METHOD = "dumpCoverageData";
@Override
public void onReceive(Context context, Intent intent) {
    try {
        Log.d(TAG, BROADCAST_RECEIVED_MESSAGE);
        Class.forName(EMMA_CLASS)
                .getMethod(EMMA_DUMP_METHOD, File.class, boolean.class,
                        boolean.class)
                .invoke(null, new File(EXEC_FILE_PATH), true,
                        false);
    } catch (Exception e) {
        Log.d(TAG, e.getMessage());
    }
}
}

在 manefist 添加(你可以添加这个调试文件夹,这样它就不会存在于主源中)

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


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


    <application>

        <receiver android:name=".CoverageReceiver">
            <intent-filter>
                <action android:name="com.example.action" />
            </intent-filter>
        </receiver>
    </application>

在应用程序的build.gradle中我添加了

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.4+"
}

model {
    android {
        compileSdkVersion 23
        buildToolsVersion "23.0.2"
    defaultConfig {
        applicationId "com.example.app"
        minSdkVersion.apiLevel 23
        targetSdkVersion.apiLevel 23
        versionCode 12
        versionName "1.11"

    }
    buildTypes {

        debug {
            testCoverageEnabled true

        }
    }

您将应用程序构建为调试应用程序,而不是安装并 运行 它。

通过ADB发送广播"adb shell am broadcast -a com.example.action"创建coverage.exec 从设备拉取覆盖范围 - adb pull /mnt/sdcard/coverage.exec

在你运行之后你需要从文件

创建覆盖
   **
 * This task is used to create a code coverage report via the Jcoco tool.
 */
task jacocoTestReport(type: JacocoReport) {
    def coverageSourceDirs = [
            'src/main/java',               
    ]
    group = "Reporting"
    description = "Generates Jacoco coverage reports"
    reports {
        csv.enabled false
        xml{
            enabled = true
            destination "${buildDir}/jacoco/jacoco.xml"
        }
        html{
            enabled true
            destination "${buildDir}/jacocoHtml"
        }
    }
    classDirectories = fileTree(
            dir: 'build/intermediates/classes',
            excludes: ['**/R.class',
                       '**/R$*.class',
                       '**/BuildConfig.*',
                       '**/Manifest*.*',
                       '**/*Activity*.*',
                       '**/*Fragment*.*'
            ]
    )
    sourceDirectories = files(coverageSourceDirs)
    executionData = files('build/coverage.exec')
}

此任务是创建覆盖率文件的一种方法 在 coverageSourceDirs 中添加应用程序源代码的所有位置,这样它就会知道要获取哪些代码并根据它们创建覆盖 executionData 是您放置从设备

中提取的 coverage.exec 的位置

运行任务 将为 html 和 xml 创建文件您还可以添加 csv(注意它将在应用程序的构建文件夹中创建)!

需要知道,您必须 运行 任务针对您构建应用程序调试版本的相同代码