Ben Ben - 20 days ago 12
Android Question

Android Gradle Jacoco: offline instrumentation for integration tests

we are building an Android app which is tested by using Appium. Now I would like to see the test coverage of our Appium tests.
I think this is possible, because Jacoco supports offline instrumentation (http://www.eclemma.org/jacoco/trunk/doc/offline.html).

And even the documentation of the jacoco gradle plugin says:


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.


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

But how do I have to write the build.gradle so our acceptance debug flavor is instrumented and the exec file is written to the Smartphone when the Appium tests are executed or even manual test cases are executed?
Because then I can extract the exec file and send it so SonarQube for further analysis.

Thanks
Ben

Ben Ben
Answer

Finally I managed it to get it working and I want to share the solution with you:

enable instrumentation for your buildType and configure SonarQube accordingly e.g.

...
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
    }
}

add the following to your AndroidManifest.xml

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

CoverageDataDumper should look like that:

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 );
      }
   }
}

Then run your Appium test cases with the acceptance flavor app (with instrumented classes). Before you call "Reset App" or "Close Application" make sure to call the following methods (just a draft, but I think you get the idea):

// 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 could be for example: /sdcard/Android/data/org.example.acceptance/files/coverage.ec

Now the Jacoco data is written to the Smartphone. Next we need to download that file. You can use

appiumDriver.pullFile( logFilePath );

Now you need to copy the file "jacoco-it.exec" (which should always be appended when you pull the file) into build/jacoco/jacoco-it.exec see gradle.build above and run

gradlew sonarRunner

In SonarQube add the Integration Test Coverage Widget and you should see now some values...

Unfortunately code coverage won't work if you are using retrolambda (as we do). Retrolambda will generate anonymous classes which are not part of the source files - so SonarQube cannot match them correctly and displays a much lower code coverage than it actually is. If someone finds a solution for that, I would be very happy :-)