UPDATE: This post is a bit outdated. To make it work with Android Gradle plugin 3.x, check my [new post about it]()! :)

Hello! In this post we’ll see how you can generate a test coverage report on an Android project including both unit tests (usually written with JUnit, Mockito, and Robolectric) and instrumented (usually written using Espresso).

Test coverage reports are an important tool to measure how much our tests actually exercise our code. Although not guaranteed a bug-free software, to have a high percentage of coverage can avoid a lot of headaches in the project.

To generate the coverage report in Android, we use Jacoco (Java Code Coverage), one of the most used tools in Java for this purpose. But the Android development environment have a particular scenario, as we have two different test artifacts, usually represented by the test folders (unit) and androidTest (instrumented).

First, let’s generate the coverage report of Espresso tests. For this example, we have a simple Activity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

  private TextView text;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    findViewById(R.id.button).setOnClickListener(this);
    findViewById(R.id.hide).setOnClickListener(this);
    text = (TextView) findViewById(R.id.text);
  }

  @Override
  public void onClick(View v) {
    if (v.getId() == R.id.button) {
      text.setText("Hello World!");
    } else {
      v.setVisibility(View.GONE);
    }
  }
}

Its layout is:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello" />

  <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Click Me!" />

  <Button
    android:id="@+id/hide"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Don't Click Me!" />

</LinearLayout>

So, let’s create a test using Espresso to ensure that the text of the TextView is changed to Hello World! when click the button:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {

  @Rule
  public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);

  @Test
  public void shouldUpdateTextAfterButtonClick() {
    onView(withId(R.id.button)).perform(click());

    onView(withId(R.id.text)).check(matches(withText("Hello World!")));
  }
}

After its execution, the execution report will be generated:

Test Execution Report

However, the coverage report is not generated yet. To enable this option, we need to add a property to our debug build variant. Using the Android plugin DSL, we can enable the coverage through the testCoverageEnabled property:

android {
  ...
  buildTypes {
    debug {
      testCoverageEnabled true
    }
    ...
  }
}

Now, just run the task createDebugCoverageReport to run the tests and generate the report.

Coverage Report

Perfect! We already have our coverage report.

Now, let’s write a test using Robolectric to test the else logic of our Activity. Side note: IMHO, I do not recommend testing the Activity and Android components using Robolectric. Use it primarily for unit tests.

So, a test with Robolectric testing the hide button behavior, whose visibility changes when clicked, would look like this:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 22)
public class MainActivityTest {

  @Test
  public void shouldHideButtonAfterClick() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);

    Button button = (Button) activity.findViewById(R.id.hide);
    button.performClick();

    assertThat(button.getVisibility(), is(View.GONE));
  }
}

By default, the Android plugin only generates the coverage report from instrumented tests. To be able to generate the coverage of unit testing, we must create a task manually:

apply plugin: 'jacoco'

task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {

  reports {
    xml.enabled = true
    html.enabled = true
  }

  def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
  def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
  def mainSrc = "${project.projectDir}/src/main/java"

  sourceDirectories = files([mainSrc])
  classDirectories = files([debugTree])
  executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")
}

EDIT: To enable the coverage report for local tests when using version 2.2.+ of Android Gradle plugin, you need to enable it in your app’s build.gradle:

android {
  ...

  testOptions {
    unitTests.all {
      jacoco {
        includeNoLocationClasses = true
      }
    }
  }
}

With the task jacocoTestReport created, now we can generate the coverage report also for unit testing.

Coverage Report

However, the problem remains: how to merge the coverage results of the two test groups?

The instrumentation performed by Jacoco produces execution files that contain the necessary data to create the report (HTML, XML, etc.). The problem here, is that Espresso generates .ec file, while the unit tests execution generates .exec file… we have different formats!

So, now the question is: how to convert from one format to another? The simple answer is: there’s no need to convert anything!

As we are not able to configure the coverage task of Espresso, we must ensure that it is executed first. Next, we need to run unit tests, and then create the coverage data with both files (ec and exec).

To enable this, we need to edit our task once more and add the coverage.ec file as a parameter in executionData property:

apply plugin: 'jacoco'

task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {

  reports {
    xml.enabled = true
    html.enabled = true
  }

  def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
  def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
  def mainSrc = "${project.projectDir}/src/main/java"

  sourceDirectories = files([mainSrc])
  classDirectories = files([debugTree])
  executionData = fileTree(dir: "$buildDir", includes: [
      "jacoco/testDebugUnitTest.exec", 
      "outputs/code-coverage/connected/*coverage.ec"
  ])
}

EDIT: As Android Gradle plugin 2.2.+ now generates a coverage file for each execution, using the device / emulator in the file name, now we need to pass every file to execution data, as the file name is now dynamic. In addition, I added the createDebugCoverageReport task as a dependency of our custom task, so we don’t need to run it manually :)

Finally, when we execute both tasks, running the Espresso tests first, we will get the unified coverage report!

gradle clean jacocoTestReport

Combined Coverage Report

An example project with all this setup can be found on Github! (EDIT: Updated with latest build tools!) #KeepCoding #KeepTesting