Skip to content

Commit 569df63

Browse files
authored
Improve support for Robolectric (#30)
To get proper Jacoco coverage when running tests with Robolectric, Jacoco's includeNoLocationClasses needs to be enabled. By making includeNoLocationClasses part of the rootCoverage extension, setting up Jacoco + Robolectric becomes a tiny bit easier. #26
1 parent 1504747 commit 569df63

File tree

12 files changed

+183
-13
lines changed

12 files changed

+183
-13
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/nl.neotech.plugin/android-root-coverage-plugin/maven-metadata.xml.svg?label=Gradle%20Plugin%20Portal)](https://plugins.gradle.org/plugin/nl.neotech.plugin.rootcoverage)
22
[![Maven Central](https://img.shields.io/maven-central/v/nl.neotech.plugin/android-root-coverage-plugin?label=Maven%20Central)](https://search.maven.org/artifact/nl.neotech.plugin/android-root-coverage-plugin)
3-
[![Build](https://github.com/NeoTech-Software/Android-Root-Coverage-Plugin/actions/workflows/build.yml/badge.svg)](https://github.com/NeoTech-Software/Android-Root-Coverage-Plugin/actions/workflows/build.yml)
3+
[![Build](https://github.com/NeoTech-Software/Android-Root-Coverage-Plugin/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/NeoTech-Software/Android-Root-Coverage-Plugin/actions/workflows/build.yml)
44

55
# Android-Root-Coverage-Plugin
66
**A Gradle plugin for combined code coverage reports for Android projects.**

gradle/dependencies.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ext {
33
minSdk : 19,
44
targetSdk : 30,
55
compileSdk: 30,
6-
kotlin : "1.4.31"
6+
kotlin : "1.4.31",
77
]
88
projectDependency = [
99

@@ -22,7 +22,8 @@ ext {
2222
espressoCore : "androidx.test.espresso:espresso-core:3.3.0",
2323
androidJUnit : "androidx.test.ext:junit:1.1.2",
2424
commonsCsv : "org.apache.commons:commons-csv:1.8",
25-
kotlinTest : "org.jetbrains.kotlin:kotlin-test:${projectVersion.kotlin}"
25+
kotlinTest : "org.jetbrains.kotlin:kotlin-test:${projectVersion.kotlin}",
26+
robolectric : "org.robolectric:robolectric:4.5.1",
2627
]
2728

2829
// Used by the plugin-version-handler.gradle
@@ -32,6 +33,6 @@ ext {
3233
"com.github.dcendents.android-maven": "2.1",
3334
"com.gradle.plugin-publish" : "0.13.0",
3435
"org.jetbrains.dokka" : "1.4.30",
35-
"com.vanniktech.maven.publish" : "0.14.2"
36+
"com.vanniktech.maven.publish" : "0.14.2",
3637
]
3738
}

plugin/src/main/kotlin/org/neotech/plugin/rootcoverage/RootCoveragePlugin.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import org.gradle.api.GradleException
99
import org.gradle.api.Plugin
1010
import org.gradle.api.Project
1111
import org.gradle.api.file.FileTree
12+
import org.gradle.api.tasks.testing.Test
1213
import org.gradle.testing.jacoco.plugins.JacocoPlugin
14+
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
1315
import org.gradle.testing.jacoco.tasks.JacocoReport
1416

1517
@Suppress("unused")
@@ -34,7 +36,10 @@ class RootCoveragePlugin : Plugin<Project> {
3436
project.plugins.apply(JacocoPlugin::class.java)
3537
}
3638

37-
project.afterEvaluate { createCoverageTaskForRoot(it) }
39+
project.afterEvaluate {
40+
it.applyConfiguration()
41+
createCoverageTaskForRoot(it)
42+
}
3843
}
3944

4045
private fun getFileFilterPatterns(): List<String> = listOf(
@@ -157,7 +162,7 @@ class RootCoveragePlugin : Plugin<Project> {
157162
task.reports.html.destination = project.file("${project.buildDir}/reports/jacoco")
158163
task.reports.xml.destination = project.file("${project.buildDir}/reports/jacoco.xml")
159164
task.reports.csv.destination = project.file("${project.buildDir}/reports/jacoco.csv")
160-
165+
161166
// Add some run-time checks.
162167
task.doFirst {
163168
it.project.allprojects.forEach { subProject ->
@@ -175,6 +180,7 @@ class RootCoveragePlugin : Plugin<Project> {
175180
// Configure the root task with sub-tasks for the sub-projects.
176181
task.project.subprojects.forEach {
177182
it.afterEvaluate { subProject ->
183+
subProject.applyConfiguration()
178184
task.addSubProject(subProject)
179185
createSubProjectCoverageTask(subProject)
180186
}
@@ -281,4 +287,21 @@ class RootCoveragePlugin : Plugin<Project> {
281287
classDirectories.from(subProject.files(javaClassTrees, kotlinClassTree))
282288
executionData.from(getExecutionDataFileTree(subProject))
283289
}
290+
291+
/**
292+
* Apply configuration from [RootCoveragePluginExtension] to the project.
293+
*/
294+
private fun Project.applyConfiguration() {
295+
tasks.withType(Test::class.java) { testTask ->
296+
testTask.extensions.findByType(JacocoTaskExtension::class.java)?.apply{
297+
isIncludeNoLocationClasses = rootProjectExtension.includeNoLocationClasses
298+
if(isIncludeNoLocationClasses) {
299+
// This Plugin is used for Android development and should support the Robolectric + Jacoco use-case
300+
// flawlessly, therefore this "bugfix" is included in the plugin codebase:
301+
// See: https://github.com/gradle/gradle/issues/5184#issuecomment-457865951
302+
excludes = listOf("jdk.internal.*")
303+
}
304+
}
305+
}
306+
}
284307
}

plugin/src/main/kotlin/org/neotech/plugin/rootcoverage/RootCoveragePluginExtension.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ open class RootCoveragePluginExtension {
1313
var buildVariant: String = "debug"
1414
var buildVariantOverrides: Map<String, String> = mutableMapOf()
1515
var excludes: List<String> = mutableListOf()
16+
17+
var includeNoLocationClasses: Boolean = false
1618

1719
/**
1820
* Same as executeTests inverted.

plugin/src/test/kotlin/org/neotech/plugin/rootcoverage/IntegrationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class IntegrationTest(
6464
report.assertCoverage("org.neotech.library.android", "LibraryAndroidKotlin")
6565
report.assertCoverage("org.neotech.app", "AppJava")
6666
report.assertCoverage("org.neotech.app", "AppKotlin")
67+
report.assertCoverage("org.neotech.app", "RobolectricTestedActivity")
6768
}
6869

6970
private fun BuildResult.assertAppCoverageReport() {
@@ -73,6 +74,7 @@ class IntegrationTest(
7374

7475
report.assertCoverage("org.neotech.app", "AppJava")
7576
report.assertCoverage("org.neotech.app", "AppKotlin")
77+
report.assertCoverage("org.neotech.app", "RobolectricTestedActivity")
7678
}
7779

7880
private fun BuildResult.assertAndroidLibraryCoverageReport() {

plugin/src/test/kotlin/org/neotech/plugin/rootcoverage/RootCoveragePluginExtensionTest.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,39 @@ class RootCoveragePluginExtensionTest {
1616
}
1717

1818
@Test
19-
fun `non default testTypes overrules include(Unit|Android)TestResults`() {
19+
fun `non default testTypes overrules include(Unit or Android)TestResults`() {
2020
// testTypes overrules includeAndroidTestResults & includeUnitTestResults (when testTypes is not default)
2121
val config = RootCoveragePluginExtension().apply {
2222
includeAndroidTestResults = true
2323
includeUnitTestResults = true
24+
@Suppress("DEPRECATION")
2425
testTypes = listOf()
2526
}
2627
assertEquals(false, config.includeUnitTestResults())
2728
assertEquals(false, config.includeAndroidTestResults())
2829
}
2930

3031
@Test
31-
fun `default testTypes does not overrule include(Unit|Android)TestResults`() {
32+
fun `default testTypes does not overrule include(Unit or Android)TestResults`() {
3233
// when testTypes is default includeAndroidTestResults & includeUnitTestResults overrule testTypes
3334
val config = RootCoveragePluginExtension().apply {
3435
includeAndroidTestResults = false
3536
includeUnitTestResults = false
37+
@Suppress("DEPRECATION")
3638
testTypes = listOf(TestVariantBuildOutput.TestType.UNIT, TestVariantBuildOutput.TestType.ANDROID_TEST)
3739
}
3840
assertEquals(false, config.includeUnitTestResults())
3941
assertEquals(false, config.includeAndroidTestResults())
4042
}
4143

4244
@Test
43-
fun `shouldExecute(Unit|Android)Tests() returns false when include(Unit|Android)TestResults() returns false`() {
45+
fun `shouldExecute(Unit or Android)Tests() returns false when include(Unit or Android)TestResults() returns false`() {
4446
// When test results are not included into the final report (`include*TestResults`), running the tests does not make sense, therefor
4547
// make sure `skip*TestExecution` returns false when this is the case.
4648
val config = RootCoveragePluginExtension().apply {
4749
includeAndroidTestResults = false
4850
includeUnitTestResults = false
51+
@Suppress("DEPRECATION")
4952
testTypes = listOf(TestVariantBuildOutput.TestType.UNIT, TestVariantBuildOutput.TestType.ANDROID_TEST)
5053
}
5154
assertEquals(false, config.includeUnitTestResults())
@@ -56,7 +59,7 @@ class RootCoveragePluginExtensionTest {
5659
}
5760

5861
@Test
59-
fun `executeTests=false overrules execute(Unit|Android)Tests`() {
62+
fun `executeTests=false overrules execute(Unit or Android)Tests`() {
6063
val config = RootCoveragePluginExtension().apply {
6164
executeTests = false
6265
}
@@ -77,7 +80,7 @@ class RootCoveragePluginExtensionTest {
7780
}
7881

7982
@Test
80-
fun `executeTests=true does not overrule execute(Unit|AndroidInstrumented)Tests`() {
83+
fun `executeTests=true does not overrule execute(Unit or AndroidInstrumented)Tests`() {
8184
val config = RootCoveragePluginExtension().apply {
8285
executeTests = true
8386
}

plugin/src/test/test-fixtures/multi-module/app/build.gradle

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ android {
3636
targetCompatibility JavaVersion.VERSION_1_8
3737
}
3838

39+
testOptions {
40+
unitTests {
41+
includeAndroidResources = true
42+
}
43+
}
44+
3945
kotlinOptions {
4046
jvmTarget = "1.8"
4147
}
@@ -47,7 +53,8 @@ dependencies {
4753
implementation projectDependency.kotlinStdlibJdk8
4854
implementation projectDependency.appCompat
4955

50-
testImplementation projectDependency.junit
56+
testImplementation projectDependency.androidJUnit
57+
testImplementation projectDependency.robolectric
5158
androidTestImplementation projectDependency.supportTestRunner
5259
androidTestImplementation projectDependency.espressoCore
5360
androidTestImplementation projectDependency.androidJUnit
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest package="org.neotech.app" />
2+
<manifest
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
package="org.neotech.app"
5+
>
6+
7+
<application>
8+
<activity
9+
android:name=".RobolectricTestedActivity"
10+
android:theme="@style/Theme.AppCompat"
11+
/>
12+
</application>
13+
14+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.neotech.app;
2+
3+
import android.os.Bundle;
4+
import android.view.View;
5+
import android.widget.TextView;
6+
7+
import androidx.annotation.Nullable;
8+
import androidx.appcompat.app.AppCompatActivity;
9+
10+
import java.util.Locale;
11+
12+
/**
13+
* Super simple activity that is unit-tested (non-instrumented) using Robolectric.
14+
*/
15+
public class RobolectricTestedActivity extends AppCompatActivity implements View.OnClickListener {
16+
17+
private TextView textViewCount;
18+
private int count = 0;
19+
20+
@Override
21+
protected void onCreate(@Nullable Bundle savedInstanceState) {
22+
super.onCreate(savedInstanceState);
23+
setContentView(R.layout.activity_robolectric_tested);
24+
25+
findViewById(R.id.button_increment_count).setOnClickListener(this);
26+
findViewById(R.id.button_decrement_count).setOnClickListener(this);
27+
textViewCount = findViewById(R.id.text_count);
28+
setCount(0);
29+
}
30+
31+
@Override
32+
public void onClick(View v) {
33+
if (v.getId() == R.id.button_increment_count) {
34+
setCount(++count);
35+
} else {
36+
setCount(--count);
37+
}
38+
}
39+
40+
public void setCount(int count) {
41+
textViewCount.setText(String.format(Locale.getDefault(), "Count: %d", count));
42+
}
43+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
android:gravity="center"
8+
android:orientation="vertical"
9+
>
10+
11+
<TextView
12+
android:id="@+id/text_count"
13+
android:layout_width="wrap_content"
14+
android:layout_height="wrap_content"
15+
tools:text="Count: 4"
16+
/>
17+
18+
<Button
19+
android:id="@+id/button_increment_count"
20+
android:layout_width="wrap_content"
21+
android:layout_height="wrap_content"
22+
android:text="Increment"
23+
tools:ignore="HardcodedText"
24+
/>
25+
26+
<Button
27+
android:id="@+id/button_decrement_count"
28+
android:layout_width="wrap_content"
29+
android:layout_height="wrap_content"
30+
android:text="Decrement"
31+
tools:ignore="HardcodedText"
32+
/>
33+
34+
</LinearLayout>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.neotech.app;
2+
3+
import android.widget.Button;
4+
import android.widget.TextView;
5+
6+
import androidx.test.core.app.ActivityScenario;
7+
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
import org.robolectric.RobolectricTestRunner;
11+
12+
import static org.junit.Assert.assertEquals;
13+
14+
/**
15+
* Super simple Robolectric activity test that is solely used to verify whether coverage is
16+
* correctly reported when enabling jacoco.includeNoLocationClasses.
17+
*/
18+
@RunWith(RobolectricTestRunner.class)
19+
public class RobolectricUnitTest {
20+
21+
@Test
22+
public void counter_is_incremented_after_increment_button_click() {
23+
ActivityScenario.launch(RobolectricTestedActivity.class).onActivity(activity -> {
24+
Button button = activity.findViewById(R.id.button_increment_count);
25+
button.performClick();
26+
27+
TextView textView = activity.findViewById(R.id.text_count);
28+
assertEquals("Count: 1", textView.getText());
29+
});
30+
}
31+
32+
@Test
33+
public void counter_is_decrement_after_decrement_button_click() {
34+
ActivityScenario.launch(RobolectricTestedActivity.class).onActivity(activity -> {
35+
Button button = activity.findViewById(R.id.button_decrement_count);
36+
button.performClick();
37+
38+
TextView textView = activity.findViewById(R.id.text_count);
39+
assertEquals("Count: -1", textView.getText());
40+
});
41+
}
42+
}

plugin/src/test/test-fixtures/multi-module/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ rootCoverage {
5050
executeTests true
5151
includeUnitTestResults true
5252
includeAndroidTestResults true
53+
includeNoLocationClasses true
5354
}

0 commit comments

Comments
 (0)