diff --git a/.travis.yml b/.travis.yml index 8b9cc921..c57fc1cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,11 +43,6 @@ script: ./gradlew check --stacktrace after_success: - ./scripts/deploy_snapshot.sh -after_failure: -- cat $TRAVIS_BUILD_DIR/tests/testCommon/build/reports/tests/test/index.html -- cat $TRAVIS_BUILD_DIR/tests/testAgp2x/build/reports/tests/test/index.html -- cat $TRAVIS_BUILD_DIR/tests/testAgp3x/build/reports/tests/test/index.html - deploy: provider: script script: ./scripts/deploy_release.sh diff --git a/README.md b/README.md index cfc5fd9d..a51d5c18 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,26 @@ A Gradle plugin that allows for the execution of [JUnit 5][junit5gh] tests in Android environments. +## Why a separate plugin? + +The JUnit Platform team provides a Gradle plugin for running JUnit 5 on the JVM. However, +this plugin is tailored to the needs of a "purely Java" application, and doesn't work in +the context of the multi-variant world that we live in on Android. Therefore, this plugin was born. + +It configures a `junitPlatformTest` task for each registered build variant of a project. +Furthermore, it automatically attaches both the Jupiter & Vintage Engines +during the execution phase of your tests as well, so there's very little configuration +necessary to get your project up-and-running on the JUnit Platform. + +Instructions on how to write JUnit 5 tests can be found [in their User Guide][junit5ug]. +Furthermore, this repository provides a small showcase of the functionality provided by JUnit 5 [here][sampletests]. + ## Download ```groovy buildscript { dependencies { - classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.21" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.22" } } ``` @@ -23,32 +37,99 @@ apply plugin: "com.android.application" apply plugin: "de.mannodermaus.android-junit5" dependencies { - testImplementation junit5() + // (Required) Writing and executing Unit Tests on the JUnit Platform. + testImplementation junit5.unitTests() - // (Optional) If you need "parameterized tests" - testImplementation junit5Params() + // (Optional) If you need "Parameterized Tests". + testImplementation junit5.parameterized() - // (Optional) For running tests inside Android Studio 3.x (see below for details) - testCompileOnly junit5EmbeddedRuntime() + // (Optional) For running tests inside Android Studio 3.x + // Please refer to the "Android Studio Workarounds" section for more insight on this. + testCompileOnly junit5.unitTestsRuntime() + + // (Optional) Writing and executing Instrumented Tests with the JUnit Platform Runner. + // + // IMPORTANT: + // By declaring this dependency, you have to use a minSdkVersion + // of at least 26, since the nature of JUnit 5 relies on APIs that aren't + // available on Android devices before then. + // Additionally, you are required to explicitly enable support for instrumented tests in the + // "junitPlatform" configuration closure (see the section below for details). + androidTestImplementation junit5.instrumentationTests() } ``` -## Usage +## Configuration -This plugin configures a `junitPlatformTest` task for each registered build variant of a project. -It automatically attaches both the Jupiter & Vintage Engines during the execution phase of your tests as well. +The plugin applies a configuration closure to your module's `android.testOptions`. +Inside it, you can use [all properties available through the default JUnit 5 Gradle plugin][junit5config]. +However, there are a few more parameters that allow for more customization of the JUnit Platform +in your Android project. These are detailed below, alongside their default values: -More instructions on how to write JUnit 5 tests can be found [in their User Guide][junit5ug]. -Furthermore, this repository provides a small showcase of the functionality provided by JUnit 5 [here][sampletests]. +```groovy +android { + testOptions { + // Configuration closure added by the plugin; + // all configurable parameters related to JUnit 5 can be found here + junitPlatform { + // The JUnit Jupiter dependency version to use + jupiterVersion "5.0.2" + + // The JUnit Vintage Engine dependency version to use + vintageVersion "4.12.2" + + // Whether or not JUnit 5 test tasks should be affected by + // JVM Arguments, System Properties & Environment Variables + // declared through "unitTests.all" closures + applyDefaultTestOptions true + + // Options related to running instrumented tests with JUnit 5. + // This is an incubating feature which utilizes the backwards-compatibility + // of the JUnit Platform in order to enhance the default Test Instrumentation Runner + // with new power. However, because of their experimental nature and steep minSdkVersion requirement, + // they are turned off by default. If you choose to enable them, you also have to declare + // the library dependency in your androidTest scope. Please refer to the "Setup" + // section for more details. + instrumentationTests { + enabled false + + // The Android-Instrumentation-Test dependency version to use + version "0.1.0" + } + + // Configuration of companion tasks for JaCoCo Reports, + // associated with each JUnit 5 task generated by the plugin. + // Just like the companion tasks themselves, these properties + // will only have an effect if your module declares the "jacoco" plugin as well. + // For each of the available report types, you can toggle the availability + // and destination folders that they write to. + jacoco { + xml { + enabled true + destination project.file() + } + html { + enabled true + destination project.file() + } + csv { + enabled true + destination project.file() + } + } + } + } +} +``` ## Android Studio Workarounds > **Note:** -> +> > The following section deals with fixing Test Execution within **Android Studio 3**. > Running your JUnit 5 tests directly from Android Studio 2.3.3 and earlier **will not work**: > You will encounter an `AbstractMethodError` when trying to do so ([more information here][as2issue]). -> +> > The cause of this error is similar in nature to the one described below, and related to outdated APIs. > Unlike that issue though, we can't fix the `AbstractMethodError` inside IntelliJ's internal runtime > in the same way. Therefore, please resort to using Gradle for unit testing in Android Studio 2. @@ -73,52 +154,13 @@ To use this, add the following line alongside the other `junit5()` dependencies: ```groovy dependencies { - testCompileOnly junit5EmbeddedRuntime() -} -``` - -## Extras - -### Override Dependency Versions - -Inside the configuration closure applied by the plugin, you can specify the same properties as you would -for a Java-based project with the JUnit Platform Gradle plugin. -However, there are some additional properties that you can apply: - -```groovy -junitPlatform { - // The JUnit Jupiter dependency version to use; matches the platform's version by default - jupiterVersion "5.0.2" - // The JUnit Vintage Engine dependency version to use; matches the platform's version by default - vintageVersion "4.12.2" -} -``` - -### JaCoCo Integration - -If the plugin detects the usage of [JaCoCo][jacoco] inside a project that it's being applied to, -it will automatically configure additional tasks to report the unit test coverage -of your application based on its JUnit 5 tests. -There is no additional setup required to enable this behaviour. -You can however customize the reports JaCoCo should generate. - -Configuration is applied through the `jacoco` clause inside the plugin's DSL: - -```groovy -apply plugin: "jacoco" - -junitPlatform { - jacoco { - csvReport true - xmlReport true - htmlReport true - } + testCompileOnly junit5.unitTestsRuntime() } ``` ## Licenses -#### `android-junit5-embedded-runtime` +#### android-junit5-embedded-runtime: ``` Copyright 2000-2016 JetBrains s.r.o. @@ -138,7 +180,7 @@ limitations under the License. See also the [full License text](android-junit5-embedded-runtime/LICENSE). -#### Others: +#### Everything else: ``` Copyright 2017 Marcel Schnelle @@ -161,7 +203,8 @@ See also the [full License text](LICENSE). [junit5gh]: https://github.com/junit-team/junit5 - [junit5ug]: http://junit.org/junit5/docs/current/user-guide + [junit5ug]: https://junit.org/junit5/docs/current/user-guide + [junit5config]: http://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle-junit-configure [travisci]: https://travis-ci.org/mannodermaus/android-junit5 [as2issue]: https://github.com/mannodermaus/android-junit5/issues/19 [jacoco]: http://www.eclemma.org/jacoco diff --git a/android-junit5-tests/common.gradle b/android-junit5-tests/common.gradle index f032f66d..ef278fb1 100644 --- a/android-junit5-tests/common.gradle +++ b/android-junit5-tests/common.gradle @@ -35,6 +35,7 @@ tasks.withType(WriteClasspathResource).all { test.testLogging { events "passed", "skipped", "failed" + exceptionFormat = "full" } dependencies { diff --git a/android-junit5-tests/testAgp2x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP2PluginSpec.groovy b/android-junit5-tests/testAgp2x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP2PluginSpec.groovy index 6e39fd74..0ece4e39 100644 --- a/android-junit5-tests/testAgp2x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP2PluginSpec.groovy +++ b/android-junit5-tests/testAgp2x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP2PluginSpec.groovy @@ -4,6 +4,7 @@ import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5JacocoReport import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5UnitTest import de.mannodermaus.gradle.plugins.junit5.util.TaskUtils import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException /* * Unit testing the integration of JUnit 5 @@ -78,7 +79,7 @@ class AGP2PluginSpec extends BasePluginSpec { expectedVariantTasks.each { assert runAllTask.getDependsOn().contains(it) } } - def "Instrumentation Test Integration: Works with Product Flavors"() { + def "Instrumentation Test Integration: Attempting to use library without enabling throws Exception"() { when: Project project = factory.newProject(rootProject()) .asAndroidApplication() @@ -86,25 +87,20 @@ class AGP2PluginSpec extends BasePluginSpec { .build() project.android { - productFlavors { - paid { - junit5InstrumentedTestsEnabled false - } - free { - junit5InstrumentedTestsEnabled true - } + testOptions.junitPlatform.instrumentationTests { + enabled = false } } + project.dependencies { + androidTestCompile junit5.instrumentationTests() + } + project.evaluate() then: - def enabledFlavor = project.android.productFlavors.getByName("free") - def enabledArgs = enabledFlavor.getTestInstrumentationRunnerArguments() - assert enabledArgs.containsKey("runnerBuilder") - - def disabledFlavor = project.android.productFlavors.getByName("paid") - def disabledArgs = disabledFlavor.getTestInstrumentationRunnerArguments() - assert !disabledArgs.containsKey("runnerBuilder") + def expect = thrown(ProjectConfigurationException) + expect.message.contains("The JUnit 5 Instrumentation Test library can only be used " + + "if support for them is explicitly enabled as well.") } } diff --git a/android-junit5-tests/testAgp3x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP3PluginSpec.groovy b/android-junit5-tests/testAgp3x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP3PluginSpec.groovy index 524626f9..04461aed 100644 --- a/android-junit5-tests/testAgp3x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP3PluginSpec.groovy +++ b/android-junit5-tests/testAgp3x/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/AGP3PluginSpec.groovy @@ -4,6 +4,7 @@ import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5JacocoReport import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5UnitTest import de.mannodermaus.gradle.plugins.junit5.util.TaskUtils import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException /* * Unit testing the integration of JUnit 5 @@ -144,7 +145,7 @@ class AGP3PluginSpec extends BasePluginSpec { project.tasks.findByName("jacocoTestReportRelease") == null } - def "Instrumentation Test Integration: Works with Product Flavors"() { + def "Instrumentation Test Integration: Attempting to use library without enabling throws Exception"() { when: Project project = factory.newProject(rootProject()) .asAndroidApplication() @@ -152,30 +153,20 @@ class AGP3PluginSpec extends BasePluginSpec { .build() project.android { - // "All flavors must now belong to a named flavor dimension" - flavorDimensions "price" - - productFlavors { - paid { - dimension "price" - junit5InstrumentedTestsEnabled false - } - free { - dimension "price" - junit5InstrumentedTestsEnabled true - } + testOptions.junitPlatform.instrumentationTests { + enabled = false } } + project.dependencies { + androidTestImplementation junit5.instrumentationTests() + } + project.evaluate() then: - def enabledFlavor = project.android.productFlavors.getByName("free") - def enabledArgs = enabledFlavor.getTestInstrumentationRunnerArguments() - assert enabledArgs.containsKey("runnerBuilder") - - def disabledFlavor = project.android.productFlavors.getByName("paid") - def disabledArgs = disabledFlavor.getTestInstrumentationRunnerArguments() - assert !disabledArgs.containsKey("runnerBuilder") + def expect = thrown(ProjectConfigurationException) + expect.message.contains("The JUnit 5 Instrumentation Test library can only be used " + + "if support for them is explicitly enabled as well.") } } diff --git a/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/BasePluginSpec.groovy b/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/BasePluginSpec.groovy index 6da5c3ee..79411743 100644 --- a/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/BasePluginSpec.groovy +++ b/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/BasePluginSpec.groovy @@ -82,14 +82,14 @@ abstract class BasePluginSpec extends Specification { project.file("build.gradle").withWriter { it.write(""" - buildscript { - dependencies { - classpath files($environment.pluginClasspathString) - } - } - - apply plugin: "de.mannodermaus.android-junit5" - """) + buildscript { + dependencies { + classpath files($environment.pluginClasspathString) + } + } + + apply plugin: "de.mannodermaus.android-junit5" + """) } def result = GradleRunner.create() @@ -108,13 +108,20 @@ abstract class BasePluginSpec extends Specification { .buildAndEvaluate() then: - def junit5 = project.dependencies.junit5 - def junit5Params = project.dependencies.junit5Params - def junit5EmbeddedRuntime = project.dependencies.junit5EmbeddedRuntime + assert project.dependencies.junit5 != null + } + + // FIXME When the deprecation is removed in a future major update, delete this test as well + def "Deprecated Dependency Handlers still work"() { + when: + Project project = factory.newProject(rootProject()) + .asAndroidApplication() + .buildAndEvaluate() - assert junit5 != null - assert junit5Params != null - assert junit5EmbeddedRuntime != null + then: + assert project.dependencies.junit5.unitTests() == project.dependencies.junit5() + assert project.dependencies.junit5.parameterized() == project.dependencies.junit5Params() + assert project.dependencies.junit5.unitTestsRuntime() == project.dependencies.junit5EmbeddedRuntime() } def "Overwrite Dependency Versions"() { @@ -123,22 +130,69 @@ abstract class BasePluginSpec extends Specification { .asAndroidApplication() .build() + project.android { + testOptions.junitPlatform { + platformVersion = "1.3.3.7" + jupiterVersion = "0.8.15" + vintageVersion = "1.2.3" + + instrumentationTests { + enabled true + version = "4.8.15" + } + } + } + + project.evaluate() + + then: + def ju5Deps = project.dependencies.junit5.unitTests() as List + assert ju5Deps.find { it.group == "org.junit.platform" && it.version == "1.3.3.7" } != null + assert ju5Deps.find { it.group == "org.junit.jupiter" && it.version == "0.8.15" } != null + assert ju5Deps.find { it.group == "org.junit.vintage" && it.version == "1.2.3" } != null + + def ju5ParamsDeps = project.dependencies.junit5.parameterized() as List + assert ju5ParamsDeps.find { it.group == "org.junit.jupiter" && it.version == "0.8.15" } != null + + def ju5InstrumentationDeps = project.dependencies.junit5.instrumentationTests() as List + assert ju5InstrumentationDeps.find { + it.group == "de.mannodermaus.junit5" && it.version == "4.8.15" + } != null + } + + // FIXME When the deprecation is removed in a future major update, delete this test as well + def "Using the old DSL to configure JUnit 5 properly delegates"() { + when: + Project project = factory.newProject(rootProject()) + .asAndroidApplication() + .build() + project.junitPlatform { platformVersion = "1.3.3.7" jupiterVersion = "0.8.15" vintageVersion = "1.2.3" + + instrumentationTests { + enabled = true + version = "4.8.15" + } } project.evaluate() then: - def ju5Deps = project.dependencies.junit5() as List + def ju5Deps = project.dependencies.junit5.unitTests() as List assert ju5Deps.find { it.group == "org.junit.platform" && it.version == "1.3.3.7" } != null assert ju5Deps.find { it.group == "org.junit.jupiter" && it.version == "0.8.15" } != null assert ju5Deps.find { it.group == "org.junit.vintage" && it.version == "1.2.3" } != null - def ju5ParamsDeps = project.dependencies.junit5Params() as List + def ju5ParamsDeps = project.dependencies.junit5.parameterized() as List assert ju5ParamsDeps.find { it.group == "org.junit.jupiter" && it.version == "0.8.15" } != null + + def ju5InstrumentationDeps = project.dependencies.junit5.instrumentationTests() as List + assert ju5InstrumentationDeps.find { + it.group == "de.mannodermaus.junit5" && it.version == "4.8.15" + } != null } def "android.testOptions: jvmArgs are properly applied"() { @@ -235,8 +289,10 @@ abstract class BasePluginSpec extends Specification { } } - project.junitPlatform { - applyDefaultTestOptions = false + project.android { + testOptions.junitPlatform { + applyDefaultTestOptions false + } } project.evaluate() @@ -260,8 +316,10 @@ abstract class BasePluginSpec extends Specification { .applyJunit5Plugin() .build() - project.junitPlatform { - reportsDir project.file("${project.buildDir.absolutePath}/other-path/test-reports") + project.android { + testOptions.junitPlatform { + reportsDir project.file("${project.buildDir.absolutePath}/other-path/test-reports") + } } project.evaluate() @@ -319,6 +377,61 @@ abstract class BasePluginSpec extends Specification { } } + project.junitPlatform { + jacoco { + xml { + enabled false + destination project.file("build/other-jacoco-folder/xml") + } + html { + enabled false + destination project.file("build/html-reports/jacoco") + } + csv { + enabled true + destination project.file("build/CSVISDABEST") + } + } + } + + project.evaluate() + + then: + // These statements automatically assert the existence of the tasks, + // and raise an Exception if absent + def runDebug = project.tasks.getByName("jacocoTestReportDebug") as AndroidJUnit5JacocoReport + def runRelease = project.tasks.getByName("jacocoTestReportRelease") + def runStaging = project.tasks.getByName("jacocoTestReportStaging") + def runAll = project.tasks.getByName("jacocoTestReport") + + // Assert that dependency chain is valid + assert runAll.getDependsOn().containsAll([runDebug, runRelease, runStaging]) + + // Assert report configuration parameters + assert runDebug.reports.xml.enabled == false + assert runDebug.reports.xml.destination.path.endsWith("build/other-jacoco-folder/xml") + assert runDebug.reports.html.enabled == false + assert runDebug.reports.html.destination.path.endsWith("build/html-reports/jacoco") + assert runDebug.reports.csv.enabled == true + assert runDebug.reports.csv.destination.path.endsWith("build/CSVISDABEST") + } + + // FIXME Deprecated. Remove test once APIs are deleted + @SuppressWarnings("GroovyPointlessBoolean") + def "Application: Jacoco Integration Using Old Configuration Parameters"() { + when: + Project project = factory.newProject(rootProject()) + .asAndroidApplication() + .applyJunit5Plugin() + .applyJacocoPlugin() + .build() + + project.android { + buildTypes { + staging {} + } + } + project.junitPlatform { jacoco { xmlReport false @@ -441,7 +554,48 @@ abstract class BasePluginSpec extends Specification { project.tasks.findByName("jacocoTestReportRelease") == null } - def "Instrumentation Test Integration: Enabled"() { + def "Instrumentation Test Integration: Doesn't attach RunnerBuilder if disabled"() { + when: + Project project = factory.newProject(rootProject()) + .asAndroidApplication() + .applyJunit5Plugin() + .build() + + project.android { + testOptions.junitPlatform.instrumentationTests { + enabled false + } + } + + project.evaluate() + + then: + def args = project.android.defaultConfig.getTestInstrumentationRunnerArguments() + assert !args.containsKey("runnerBuilder") + } + + def "Instrumentation Test Integration: Attaches RunnerBuilder"() { + when: + Project project = factory.newProject(rootProject()) + .asAndroidApplication() + .applyJunit5Plugin() + .build() + + project.android { + testOptions.junitPlatform.instrumentationTests { + enabled true + } + } + + project.evaluate() + + then: + def args = project.android.defaultConfig.getTestInstrumentationRunnerArguments() + assert args.containsKey("runnerBuilder") + assert args["runnerBuilder"].contains("AndroidJUnit5Builder") + } + + def "Instrumentation Test Integration: Appends RunnerBuilder if another is already present"() { when: Project project = factory.newProject(rootProject()) .asAndroidApplication() @@ -450,7 +604,11 @@ abstract class BasePluginSpec extends Specification { project.android { defaultConfig { - junit5InstrumentedTestsEnabled true + testInstrumentationRunnerArgument "runnerBuilder", "com.something.else.OtherRunnerBuilder" + } + + testOptions.junitPlatform.instrumentationTests { + enabled true } } @@ -459,5 +617,9 @@ abstract class BasePluginSpec extends Specification { then: def args = project.android.defaultConfig.getTestInstrumentationRunnerArguments() assert args.containsKey("runnerBuilder") + + // Intentional comma + assert args["runnerBuilder"].contains("com.something.else.OtherRunnerBuilder,") + assert args["runnerBuilder"].contains("AndroidJUnit5Builder") } } diff --git a/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/CallableGroovyTests.groovy b/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/CallableGroovyTests.groovy index 8a07f65e..a300a412 100644 --- a/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/CallableGroovyTests.groovy +++ b/android-junit5-tests/testCommon/src/test/groovy/de/mannodermaus/gradle/plugins/junit5/CallableGroovyTests.groovy @@ -4,8 +4,8 @@ import org.junit.Test class CallableGroovyTests { @Test - void callable() { - def obj = new Callable({ 2 + 2 }) + void callable0() { + def obj = new Callable0({ 2 + 2 }) assert obj() == 4 } diff --git a/android-junit5-tests/testCommon/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/CallableKotlinTests.kt b/android-junit5-tests/testCommon/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/CallableKotlinTests.kt index 9377f3e3..dc29ad9a 100644 --- a/android-junit5-tests/testCommon/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/CallableKotlinTests.kt +++ b/android-junit5-tests/testCommon/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/CallableKotlinTests.kt @@ -4,8 +4,8 @@ import org.junit.Test class CallableKotlinTests { @Test - fun callable() { - val obj = Callable { 2 + 2 } + fun callable0() { + val obj = Callable0 { 2 + 2 } assert(obj() == 4) } diff --git a/android-junit5/build.gradle b/android-junit5/build.gradle index e13ab148..5d8117de 100644 --- a/android-junit5/build.gradle +++ b/android-junit5/build.gradle @@ -48,11 +48,12 @@ gradlePlugin { import org.apache.tools.ant.filters.ReplaceTokens processResources { - def tokens = [ANDROID_JUNIT5_VERSION: PLUGIN_RUNTIME_VERSION_NAME, - JUNIT4_VERSION : JUNIT4_VERSION, - JUNIT_PLATFORM_VERSION: JUNIT_PLATFORM_VERSION, - JUNIT_JUPITER_VERSION : JUNIT_JUPITER_VERSION, - JUNIT_VINTAGE_VERSION : JUNIT_VINTAGE_VERSION] + def tokens = [ANDROID_JUNIT5_VERSION : PLUGIN_RUNTIME_VERSION_NAME, + JUNIT4_VERSION : JUNIT4_VERSION, + INSTRUMENTATION_TEST_VERSION: INSTRUMENTATION_VERSION_NAME, + JUNIT_PLATFORM_VERSION : JUNIT_PLATFORM_VERSION, + JUNIT_JUPITER_VERSION : JUNIT_JUPITER_VERSION, + JUNIT_VINTAGE_VERSION : JUNIT_VINTAGE_VERSION] inputs.properties(tokens) diff --git a/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/AndroidJUnitPlatformExtension.groovy b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/AndroidJUnitPlatformExtension.groovy index 7519c6be..19359087 100644 --- a/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/AndroidJUnitPlatformExtension.groovy +++ b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/AndroidJUnitPlatformExtension.groovy @@ -1,6 +1,8 @@ package de.mannodermaus.gradle.plugins.junit5 +import com.android.annotations.NonNull import org.gradle.api.Project +import org.gradle.util.ConfigureUtil import org.junit.platform.gradle.plugin.JUnitPlatformExtension import javax.annotation.Nullable @@ -15,16 +17,17 @@ import javax.annotation.Nullable class AndroidJUnitPlatformExtension extends JUnitPlatformExtension { + private final Project project + AndroidJUnitPlatformExtension(Project project) { super(project) + this.project = project } - /** - * The version of JUnit Jupiter to use.*/ + /** The version of JUnit Jupiter to use.*/ @Nullable String jupiterVersion - /** - * The version of JUnit Vintage Engine to use. */ + /** The version of JUnit Vintage Engine to use. */ @Nullable String vintageVersion @@ -36,6 +39,166 @@ class AndroidJUnitPlatformExtension extends JUnitPlatformExtension { * by a "testOptions.unitTests.all" closure: * * - jvmArgs - * - systemProperties */ - boolean applyDefaultTestOptions = true + * - systemProperties + * - environment variables */ + private boolean applyDefaultTestOptions = true + + void applyDefaultTestOptions(boolean enabled) { + this.applyDefaultTestOptions = enabled + } + + boolean getApplyDefaultTestOptions() { + return applyDefaultTestOptions + } + + /* Integration of Instrumentation Tests */ + + /** + * Options for controlling instrumentation test execution with JUnit 5. + * + * @since 1.0.22 + */ + private final InstrumentationTestOptions instrumentationTests = new InstrumentationTestOptions() + + /** + * Configures instrumentation test options. + * + * @since 1.0.22 + */ + void instrumentationTests(Closure closure) { + ConfigureUtil.configure(closure, instrumentationTests) + } + + /** + * Configures instrumentation test options. + * + * @since 1.0.22 + */ + @NonNull + InstrumentationTestOptions getInstrumentationTests() { return instrumentationTests } + + /** + * Options for controlling instrumentation test execution.*/ + static class InstrumentationTestOptions { + + /** Whether or not to enable support for JUnit 5 instrumentation tests. */ + private boolean enabled = false + + /** The version of the instrumentation companion library to use. */ + @Nullable String version + + void enabled(boolean state) { + this.enabled = state + } + + boolean getEnabled() { + return enabled + } + } + + /* Integration of Jacoco Reporting */ + + /** + * Options for controlling Jacoco reporting.*/ + private final JacocoOptions jacoco = new JacocoOptions(project) + + /** + * Configures Jacoco reporting options.*/ + void jacoco(Closure closure) { + ConfigureUtil.configure(closure, jacoco) + } + + /** + * Configures Jacoco reporting options.*/ + JacocoOptions getJacoco() { return jacoco } + + /** + * Options for controlling Jacoco reporting.*/ + static class JacocoOptions { + + private final Project project + private final Report html + private final Report csv + private final Report xml + + JacocoOptions(Project project) { + this.project = project + this.html = new Report() + this.csv = new Report() + this.xml = new Report() + } + + void html(Closure closure) { + ConfigureUtil.configure(closure, html) + } + + Report getHtml() { + return html + } + + void csv(Closure closure) { + ConfigureUtil.configure(closure, csv) + } + + Report getCsv() { + return csv + } + + void xml(Closure closure) { + ConfigureUtil.configure(closure, xml) + } + + Report getXml() { + return xml + } + + // FIXME DEPRECATED --------------------------------------------------------------- + def htmlReport(boolean state) { + logDeprecationWarning("htmlReport", "html.enabled") + html.enabled = state + } + + def csvReport(boolean state) { + logDeprecationWarning("csvReport", "csv.enabled") + csv.enabled = state + } + + def xmlReport(boolean state) { + logDeprecationWarning("xmlReport", "xml.enabled") + xml.enabled = state + } + + private def logDeprecationWarning(String dontUse, String useInstead) { + LogUtils.agpStyleLog(project.logger, + LogUtils.Level.WARNING, + "Accessing the Jacoco property '$dontUse' for JUnit 5 configuration " + "is deprecated and will be removed in a future version. Please use '$useInstead' instead") + } + + // END DEPRECATED ----------------------------------------------------------------- + + class Report { + + private boolean enabled = true + + @Nullable + private File destination + + void enabled(boolean state) { + this.enabled = state + } + + boolean isEnabled() { + return enabled + } + + void destination(File destination) { + this.destination = destination + } + + @Nullable + File getDestination() { + return destination + } + } + } } diff --git a/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/ExtensionProxy.groovy b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/ExtensionProxy.groovy new file mode 100644 index 00000000..a39938ef --- /dev/null +++ b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/ExtensionProxy.groovy @@ -0,0 +1,41 @@ +package de.mannodermaus.gradle.plugins.junit5 + +import org.gradle.api.Project + +/** + * Temporary Proxy class, used to promote the new way of specifying + * JUnit Platform config parameters through android.testOptions, + * while still allowing the "old way" to co-exist for a bit.*/ +class ExtensionProxy { + + static def warning = "You're using the old way of configuring JUnit 5 in your project. " + + "This is deprecated behavior and subject to removal in a future version of the plugin. " + + "Please move your 'junitPlatform' clause into 'android.testOptions'!" + + private final Project project + private final AndroidJUnitPlatformExtension delegate + + ExtensionProxy(Project project, AndroidJUnitPlatformExtension delegate) { + this.project = project + this.delegate = delegate + } + + Object methodMissing(String methodName, Object args) { + logWarning() + return delegate.invokeMethod(methodName, args) + } + + Object propertyMissing(String propertyName) { + logWarning() + return delegate.getProperty(propertyName) + } + + def propertyMissing(String propertyName, Object value) { + logWarning() + delegate.setProperty(propertyName, value) + } + + private def logWarning() { + LogUtils.agpStyleLog(project.logger, LogUtils.Level.WARNING, warning) + } +} diff --git a/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/LogUtils.groovy b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/LogUtils.groovy new file mode 100644 index 00000000..47e16034 --- /dev/null +++ b/android-junit5/src/main/groovy/de/mannodermaus/gradle/plugins/junit5/LogUtils.groovy @@ -0,0 +1,30 @@ +package de.mannodermaus.gradle.plugins.junit5 + +import org.gradle.api.logging.Logger + +/** + * Note: This is in Groovy until no other code in this language + * is using it anymore, or no more groovy files remain.*/ +class LogUtils { + + static def agpStyleLog(Logger logger, Level level, String message) { + def fullMessage = "AGPBI: {\"kind\":\"$level.tag\",\"text\":\"$message\"}" + + if (level == Level.WARNING) { + logger.warn(fullMessage) + } else { + logger.info(fullMessage) + } + } + + enum Level { + INFO("info"), + WARNING("warning"); + + private final String tag + + Level(String tag) { + this.tag = tag + } + } +} diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Constants.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Constants.kt index a55120a3..e8ff6e2f 100644 --- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Constants.kt +++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Constants.kt @@ -18,15 +18,14 @@ const val JUNIT_PLATFORM_VERSION_PROP = "junitPlatformVersion" const val JUNIT_JUPITER_VERSION_PROP = "junitJupiterVersion" const val JUNIT_VINTAGE_VERSION_PROP = "junitVintageVersion" const val JUNIT4_VERSION_PROP = "junit4Version" +const val INSTRUMENTATION_TEST_VERSION_PROP = "instrumentationTestVersion" // Instrumentation Test integration const val RUNNER_BUILDER_ARG = "runnerBuilder" const val JUNIT5_RUNNER_BUILDER_CLASS_NAME = "de.mannodermaus.junit5.AndroidJUnit5Builder" // Dependency Handler Names -const val DEP_HANDLER_NAME_JUNIT5 = "junit5" -const val DEP_HANDLER_NAME_PARAMETERIZED = "junit5Params" -const val DEP_HANDLER_NAME_RUNTIME = "junit5EmbeddedRuntime" +const val DEP_HANDLER_NAME = "junit5" // Configuration Names const val PARAM_NAME_ENABLE_INSTRUMENTED_TESTS = "junit5InstrumentedTestsEnabled" diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Dependencies.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Dependencies.kt index 34e169cf..498bab83 100644 --- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Dependencies.kt +++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Dependencies.kt @@ -1,16 +1,144 @@ package de.mannodermaus.gradle.plugins.junit5 +import de.mannodermaus.gradle.plugins.junit5.LogUtils.Level +import groovy.lang.Closure import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException import org.gradle.api.artifacts.Dependency +import org.gradle.api.logging.Logger import java.util.Properties /* * Model classes holding information about the transitive dependencies of the plugin, - * exposed to consumers through its custom dependency handlers. + * exposed to consumers through the custom dependency handler. */ +/* Extensions */ + +private fun Logger.replacementWarning(oldName: String, newName: String) { + this.agpStyleLog( + message = "The JUnit 5 dependency on '$oldName' " + + "is deprecated and will be removed in a future version. Please use '$newName' instead!", + level = Level.WARNING) +} + +/* Types */ + /** - * Data holder, serving as a gateway to the actual dependencies via its properties. + * Public-facing handler object, injected into the default DependencyHandler, + * exposing the different available methods to consumers. + */ +@Suppress("MemberVisibilityCanPrivate") +class JUnit5DependencyHandler( + private val project: Project, + defaults: Properties) : Closure(null) /* FIXME Part of junit5 deprecation */ { + + private val versions: Versions by lazy { + Versions( + project = project, + extension = project.junit5, + defaults = defaults) + } + + /* Public */ + + /** + * Retrieves the list of dependencies related to + * running Unit Tests on the JUnit Platform with Android. + */ + fun unitTests() = listOf( + versions.others.junit4, + versions.jupiter.api, + versions.platform.engine, + versions.jupiter.engine, + versions.vintage.engine, + + // Only needed to run tests in an Android Studio that bundles an older version + // (see also http://junit.org/junit5/docs/current/user-guide/#running-tests-ide-intellij-idea) + versions.platform.launcher, + versions.platform.console + ) + + /** + * Retrieves the list of dependencies related to + * writing Parameterized Tests. + */ + fun parameterized() = listOf( + versions.jupiter.params + ) + + /** + * Retrieves the list of dependencies related to + * executing Unit Tests in Android Studio 3 properly. + */ + fun unitTestsRuntime() = listOf( + versions.others.embeddedRuntime + ) + + /** + * Retrieves the list of dependencies related to + * running Instrumentation Tests on the JUnit Platform with Android. + */ + fun instrumentationTests(): List { + // Abort if JUnit 5 Instrumentation Tests aren't enabled, + // since that would cause confusion otherwise. + if (!project.junit5.instrumentationTests.enabled) { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + throw ProjectConfigurationException( + "The JUnit 5 Instrumentation Test library can only be used " + + "if support for them is explicitly enabled as well.\n" + + "Please add the following to your build.gradle\n:" + + "android.defaultConfig.testOptions.instrumentationTests.enabled = true", null) + } + + return listOf(versions.others.instrumentationTest) + } + + /* Internal */ + + internal fun configure() { + // "dependencies.junit5" is the gateway to the sharded dependency groups + project.dependencies.ext[DEP_HANDLER_NAME] = this + + // FIXME Deprecation ---------------------------------------------------------------------------------------------------- + // "dependencies.junit5()" is the old way to specify unit tests + // this backwards compatibility is realized through the invoke() operator + // (see class declaration!) + + // "dependencies.junit5Params()" is the old way to specify parameterized tests + project.dependencies.ext["junit5Params"] = Callable0 { + project.logger.replacementWarning(oldName = "junit5Params()", + newName = "junit5.parameterized()") + this.parameterized() + } + + // "dependencies.junit5EmbeddedRuntime()" is the old way to specify the embedded runtime + project.dependencies.ext["junit5EmbeddedRuntime"] = Callable0 { + project.logger.replacementWarning( + oldName = "junit5EmbeddedRuntime()", + newName = "junit5.unitTestsRuntime()") + this.unitTestsRuntime() + } + } + + // "dependencies.junit5()" is the old way to specify unit tests + @Suppress("MemberVisibilityCanPrivate") + operator fun invoke(): List { + project.logger.replacementWarning(oldName = "junit5()", newName = "junit5.unitTests()") + return this.unitTests() + } + + @Suppress("unused") + fun doCall(): List { + return this() + } + // END Deprecation ---------------------------------------------------------------------------------------------------- +} + +/* Internal API */ + +/** + * Internal data holder, serving as a gateway to the actual dependencies via its properties. */ class Versions( project: Project, @@ -20,7 +148,7 @@ class Versions( val jupiter = Jupiter(project, extension, defaults) val platform = Platform(project, extension, defaults) val vintage = Vintage(project, extension, defaults) - val others = Other(project, defaults) + val others = Other(project, extension, defaults) } abstract class BaseDependency(private val project: Project) { @@ -96,6 +224,7 @@ class Vintage( */ class Other( project: Project, + private val extension: AndroidJUnitPlatformExtension, properties: Properties ) : BaseDependency(project) { @@ -108,4 +237,12 @@ class Other( groupId = "junit", artifactId = "junit", version = properties.getProperty(JUNIT4_VERSION_PROP)) + + val instrumentationTest by lazy { + dependency( + groupId = "de.mannodermaus.junit5", + artifactId = "android-instrumentation-test", + version = extension.instrumentationTests.version ?: + properties.getProperty(INSTRUMENTATION_TEST_VERSION_PROP)) + } } diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Extensions.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Extensions.kt index e4b24b64..2719ef51 100644 --- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Extensions.kt +++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Extensions.kt @@ -4,8 +4,9 @@ import com.android.build.gradle.BaseExtension import com.android.build.gradle.api.BaseVariant import com.android.build.gradle.api.UnitTestVariant import com.android.build.gradle.internal.api.TestedVariant -import com.android.builder.model.ProductFlavor -import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5JacocoReport +import com.android.build.gradle.internal.dsl.TestOptions +import de.mannodermaus.gradle.plugins.junit5.LogUtils.Level +import de.mannodermaus.gradle.plugins.junit5.LogUtils.Level.INFO import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5UnitTest import groovy.lang.Closure import org.gradle.api.GradleException @@ -41,6 +42,23 @@ fun loadProperties(resource: String): Properties { return properties } +/** + * Adds the provided key-value pair to the Map. + * If there already is a value associated with the key, + * the value is appended to the end of the current value + * using the given delimiter. + */ +fun MutableMap.append( + key: String, value: String, delimiter: String = ","): String? { + val insertedValue = if (containsKey(key)) { + "${this[key]}$delimiter$value" + } else { + value + } + + return this.put(key, insertedValue) +} + /* * "Extension" Extension Functions: * Shorthand properties to access different plugins' extension models. @@ -61,10 +79,7 @@ val FiltersExtension.packages val FiltersExtension.engines get() = extensionByName(ENGINES_EXTENSION_NAME) -val AndroidJUnitPlatformExtension.jacoco - get() = extensionByName(JACOCO_EXTENSION_NAME) - -val Project.junit5 +val TestOptions.junitPlatform get() = extensionByName(EXTENSION_NAME) val Project.jacoco @@ -73,15 +88,21 @@ val Project.jacoco val AndroidJUnit5UnitTest.jacoco get() = extensionByName("jacoco") +/* Extensions for Gradle */ + +fun Logger.agpStyleLog(message: String, level: Level = INFO) { + LogUtils.agpStyleLog(this, level, message) +} + /* Interoperability layer for Gradle */ /** * Create & add an Extension to the given container by name. */ -inline fun Any.createExtension( +inline fun Any.extend( name: String, args: Array = emptyArray(), - noinline init: (T.() -> Unit)? = null): T { + noinline init: ((T) -> Unit)? = null): T { // Access the Extension container of an object, // or raise an Exception if none are available if (this !is ExtensionAware) { @@ -89,7 +110,7 @@ inline fun Any.createExtension( } // Create & Configure the new extension - val created = this.extensions.create(name, T::class.java, *args) + val created: T = this.extensions.create(name, T::class.java, *args) init?.let { init(created) } return created } @@ -125,6 +146,9 @@ fun Project.hasPlugin(name: String) = this.plugins.findPlugin(name) != null val Project.android: BaseExtension get() = this.extensions.getByName("android") as BaseExtension +val Project.junit5: AndroidJUnitPlatformExtension + get() = this.android.testOptions.junitPlatform + /** * Access the extra properties of a DependencyHandler. * Equivalent to "DependencyHandler#ext" in Groovy. @@ -136,17 +160,6 @@ val DependencyHandler.ext: ExtraPropertiesExtension ExtraPropertiesExtension.EXTENSION_NAME) as ExtraPropertiesExtension } -/** - * Access the extra properties of a ProductFlavor. - * Equivalent to "ProductFlavor#ext" in Groovy. - */ -val ProductFlavor.ext: ExtraPropertiesExtension - get() { - val aware = this as ExtensionAware - return aware.extensions.getByName( - ExtraPropertiesExtension.EXTENSION_NAME) as ExtraPropertiesExtension - } - val BaseVariant.unitTestVariant: UnitTestVariant get() { if (this !is TestedVariant) { @@ -196,7 +209,7 @@ fun Project.withDependencies(defaults: Properties, config: (Versions) -> Any): A * Multi-language functional construct, * mapped to Groovy's dynamic Closures as well as Kotlin's invoke syntax. * - * A [Callable] can be invoked with the short-hand + * A [Callable0] can be invoked with the short-hand * function syntax from both Kotlin & Groovy: * *
@@ -210,19 +223,19 @@ fun Project.withDependencies(defaults: Properties, config: (Versions) -> Any): A
  * 
*/ @Suppress("unused") -class Callable(private val body: () -> Any) : Closure(null) { +class Callable0(private val body: () -> R) : Closure(null) { /** Kotlin's call syntax */ - operator fun invoke(): Any = body() + operator fun invoke(): R = body() /** Groovy's call syntax */ - fun doCall(): Any = body() + fun doCall(): R = body() } /** * Multi-language functional construct, * mapped to Groovy's dynamic Closures as well as Kotlin's invoke syntax. * - * A [Callable] can be invoked with the short-hand + * A [Callable1] can be invoked with the short-hand * function syntax from both Kotlin & Groovy: * *
diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt
index ac6a8f50..f108aad3 100644
--- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt
+++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/Plugin.kt
@@ -1,7 +1,6 @@
 package de.mannodermaus.gradle.plugins.junit5
 
 import com.android.build.gradle.api.BaseVariant
-import de.mannodermaus.gradle.plugins.junit5.integrations.attachInstrumentationTestSupport
 import de.mannodermaus.gradle.plugins.junit5.providers.DirectoryProvider
 import de.mannodermaus.gradle.plugins.junit5.providers.JavaDirectoryProvider
 import de.mannodermaus.gradle.plugins.junit5.providers.KotlinDirectoryProvider
@@ -35,22 +34,27 @@ class AndroidJUnitPlatformPlugin : Plugin {
     project.configureDependencies()
     project.afterEvaluate {
       it.configureTasks()
+      it.applyConfigurationParameters()
     }
   }
 
   private fun Project.configureExtensions() {
-    createExtension(EXTENSION_NAME, arrayOf(this)) {
-      createExtension(SELECTORS_EXTENSION_NAME)
-      createExtension(FILTERS_EXTENSION_NAME) {
-        createExtension(PACKAGES_EXTENSION_NAME)
-        createExtension(TAGS_EXTENSION_NAME)
-        createExtension(ENGINES_EXTENSION_NAME)
-      }
-      createExtension(JACOCO_EXTENSION_NAME)
-    }
-
-    // Connect with integration libraries
-    attachInstrumentationTestSupport()
+    // Hook the JUnit Platform configuration into the Android testOptions
+    android.testOptions
+        .extend(EXTENSION_NAME, arrayOf(this)) { ju5 ->
+          ju5.extend(SELECTORS_EXTENSION_NAME)
+          ju5.extend(FILTERS_EXTENSION_NAME) { filters ->
+            filters.extend(PACKAGES_EXTENSION_NAME)
+            filters.extend(TAGS_EXTENSION_NAME)
+            filters.extend(ENGINES_EXTENSION_NAME)
+          }
+        }
+
+    // FIXME Deprecated --------------------------------------------------------------------------------
+    // For backwards compatibility, still offer the "old" entry point "project.junitPlatform",
+    // which should redirect to the testOptions-based DSL dynamically
+    this.extend(EXTENSION_NAME, arrayOf(this, this.junit5))
+    // END Deprecation  --------------------------------------------------------------------------------
   }
 
   private fun Project.configureDependencies() {
@@ -71,31 +75,9 @@ class AndroidJUnitPlatformPlugin : Plugin {
       }
     }
 
-    // Create the dependency handlers for JUnit 5
-    project.dependencies.ext[DEP_HANDLER_NAME_JUNIT5] = Callable {
-      withDependencies(defaults) {
-        listOf(
-            it.others.junit4,
-            it.jupiter.api,
-            it.platform.engine,
-            it.jupiter.engine,
-            it.vintage.engine,
-
-            // Only needed to run tests in an Android Studio that bundles an older version
-            // (see also http://junit.org/junit5/docs/current/user-guide/#running-tests-ide-intellij-idea)
-            it.platform.launcher,
-            it.platform.console
-        )
-      }
-    }
-
-    project.dependencies.ext[DEP_HANDLER_NAME_PARAMETERIZED] = Callable {
-      withDependencies(defaults) { it.jupiter.params }
-    }
-
-    project.dependencies.ext[DEP_HANDLER_NAME_RUNTIME] = Callable {
-      withDependencies(defaults) { it.others.embeddedRuntime }
-    }
+    // Create the custom dependency endpoints for JUnit 5
+    val dependencyHandler = JUnit5DependencyHandler(this, defaults)
+    dependencyHandler.configure()
   }
 
   private fun Project.configureTasks() {
@@ -131,4 +113,15 @@ class AndroidJUnitPlatformPlugin : Plugin {
 
     return providers
   }
+
+  private fun Project.applyConfigurationParameters() {
+    // Consume Instrumentation Test options &
+    // apply configuration if enabled
+    if (junit5.instrumentationTests.enabled) {
+      // Attach the JUnit 5 RunnerBuilder automatically
+      // to the test instrumentation runner's parameters.
+      val runnerArgs = android.safeDefaultConfig.testInstrumentationRunnerArguments
+      runnerArgs.append(RUNNER_BUILDER_ARG, JUNIT5_RUNNER_BUILDER_CLASS_NAME)
+    }
+  }
 }
diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/integrations/InstrumentationTests.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/integrations/InstrumentationTests.kt
deleted file mode 100644
index a25cbf8e..00000000
--- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/integrations/InstrumentationTests.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.mannodermaus.gradle.plugins.junit5.integrations
-
-import com.android.builder.model.ProductFlavor
-import de.mannodermaus.gradle.plugins.junit5.Callable1
-import de.mannodermaus.gradle.plugins.junit5.JUNIT5_RUNNER_BUILDER_CLASS_NAME
-import de.mannodermaus.gradle.plugins.junit5.PARAM_NAME_ENABLE_INSTRUMENTED_TESTS
-import de.mannodermaus.gradle.plugins.junit5.RUNNER_BUILDER_ARG
-import de.mannodermaus.gradle.plugins.junit5.android
-import de.mannodermaus.gradle.plugins.junit5.ext
-import de.mannodermaus.gradle.plugins.junit5.safeDefaultConfig
-import org.gradle.api.Project
-
-/**
- * Extends the default Android plugin models
- * to allow for configuration of parameters
- * related to the Instrumentation Test companion library.
- */
-fun Project.attachInstrumentationTestSupport() {
-  // Attach extensions to both the default configuration
-  // as well as all product flavors, so that users can
-  // enable Instrumentation Test support for the scope they want
-  this.android.safeDefaultConfig.attachConfigureMethod()
-
-  // Can't merge this with the default config b/c
-  // we have to use "all()" here to auto-configure
-  // any lazily appended flavor
-  this.android.productFlavors.all { it.attachConfigureMethod() }
-}
-
-private fun ProductFlavor.attachConfigureMethod() {
-  this.ext[PARAM_NAME_ENABLE_INSTRUMENTED_TESTS] = Callable1 { enabled ->
-    if (enabled) {
-      this.testInstrumentationRunnerArguments[RUNNER_BUILDER_ARG] = JUNIT5_RUNNER_BUILDER_CLASS_NAME
-    } else {
-      this.testInstrumentationRunnerArguments.remove(RUNNER_BUILDER_ARG)
-    }
-  }
-}
diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Base.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Base.kt
index 360bffbc..80587e9b 100644
--- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Base.kt
+++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Base.kt
@@ -3,7 +3,6 @@ package de.mannodermaus.gradle.plugins.junit5.tasks
 import com.android.build.gradle.api.BaseVariant
 import com.android.build.gradle.internal.scope.TaskConfigAction
 import com.android.build.gradle.internal.scope.VariantScope
-import de.mannodermaus.gradle.plugins.junit5.junit5
 import de.mannodermaus.gradle.plugins.junit5.variantData
 import org.gradle.api.Project
 import org.gradle.api.Task
@@ -19,5 +18,4 @@ abstract class JUnit5TaskConfigAction(
 
   protected val variant: BaseVariant = testTask.variant
   protected val scope: VariantScope = variant.variantData.scope
-  protected val junit5 = project.junit5
 }
diff --git a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Jacoco.kt b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Jacoco.kt
index 6cb51bbc..ccbf00aa 100644
--- a/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Jacoco.kt
+++ b/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/tasks/Jacoco.kt
@@ -1,6 +1,7 @@
 package de.mannodermaus.gradle.plugins.junit5.tasks
 
 import de.mannodermaus.gradle.plugins.junit5.jacoco
+import de.mannodermaus.gradle.plugins.junit5.junit5
 import de.mannodermaus.gradle.plugins.junit5.junit5Info
 import de.mannodermaus.gradle.plugins.junit5.maybeCreate
 import de.mannodermaus.gradle.plugins.junit5.providers.DirectoryProvider
@@ -16,6 +17,7 @@ private const val GROUP_REPORTING = "reporting"
  * Jacoco Test Reporting Task connected to a variant-aware JUnit 5 task.
  * Required to be "open" in order for Groovy's proxy magic to do its thing.
  */
+@Suppress("MemberVisibilityCanPrivate")
 open class AndroidJUnit5JacocoReport : JacocoReport() {
 
   companion object {
@@ -27,18 +29,6 @@ open class AndroidJUnit5JacocoReport : JacocoReport() {
     }
   }
 
-  /**
-   * Configuration exposed to consumers
-   */
-  open class Extension {
-    /** Generate a test coverage report in CSV */
-    var csvReport = true
-    /** Generate a test coverage report in XML */
-    var xmlReport = true
-    /** Generate a test coverage report in HTML */
-    var htmlReport = true
-  }
-
   /**
    * Configuration closure for an Android JUnit5 Jacoco Report task.
    */
@@ -68,11 +58,15 @@ open class AndroidJUnit5JacocoReport : JacocoReport() {
       reportTask.sourceDirectories = project.files(directoryProviders.mainSourceDirectories())
 
       // Apply JUnit 5 configuration parameters
-      val junit5Jacoco = junit5.jacoco
-      reportTask.reports.apply {
-        csv.isEnabled = junit5Jacoco.csvReport
-        html.isEnabled = junit5Jacoco.htmlReport
-        xml.isEnabled = junit5Jacoco.xmlReport
+      val junit5Jacoco = project.junit5.jacoco
+      val allReports = listOf(
+          junit5Jacoco.csv to reportTask.reports.csv,
+          junit5Jacoco.xml to reportTask.reports.xml,
+          junit5Jacoco.html to reportTask.reports.html)
+
+      allReports.forEach { (from, to) ->
+        to.isEnabled = from.isEnabled
+        from.destination?.let { to.destination = it }
       }
 
       project.logger.junit5Info(
diff --git a/android-junit5/src/main/resources/de/mannodermaus/gradle/plugins/junit5/versions.properties b/android-junit5/src/main/resources/de/mannodermaus/gradle/plugins/junit5/versions.properties
index c6043200..3df4a1d5 100644
--- a/android-junit5/src/main/resources/de/mannodermaus/gradle/plugins/junit5/versions.properties
+++ b/android-junit5/src/main/resources/de/mannodermaus/gradle/plugins/junit5/versions.properties
@@ -1,8 +1,9 @@
 # suppress inspection "UnusedProperty" for whole file
 # (Populated from a Gradle "process resources" task)
-androidJunit5Version = @ANDROID_JUNIT5_VERSION@
-junit4Version        = @JUNIT4_VERSION@
+androidJunit5Version       = @ANDROID_JUNIT5_VERSION@
+junit4Version              = @JUNIT4_VERSION@
+instrumentationTestVersion = @INSTRUMENTATION_TEST_VERSION@
 
-junitPlatformVersion = @JUNIT_PLATFORM_VERSION@
-junitJupiterVersion  = @JUNIT_JUPITER_VERSION@
-junitVintageVersion  = @JUNIT_VINTAGE_VERSION@
+junitPlatformVersion       = @JUNIT_PLATFORM_VERSION@
+junitJupiterVersion        = @JUNIT_JUPITER_VERSION@
+junitVintageVersion        = @JUNIT_VINTAGE_VERSION@
diff --git a/gradle.properties b/gradle.properties
index bcafc0d7..e96a2cf2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,7 +12,7 @@ LICENSE_NAME                                = Apache-2.0
 PLUGIN_GROUP_ID                             = de.mannodermaus.gradle.plugins
 PLUGIN_ARTIFACT_ID                          = android-junit5
 PLUGIN_DESCRIPTION                          = Unit Testing with JUnit 5 for Android.
-PLUGIN_RUNTIME_VERSION_NAME                 = 1.0.22-SNAPSHOT
+PLUGIN_RUNTIME_VERSION_NAME                 = 1.0.22
 
 # Artifact configuration (embedded-runtime)
 RUNTIME_GROUP_ID                            = de.mannodermaus.gradle.plugins
@@ -23,7 +23,7 @@ RUNTIME_DESCRIPTION                         = Mirror of IntelliJ IDEA's embedded
 INSTRUMENTATION_GROUP_ID                    = de.mannodermaus.junit5
 INSTRUMENTATION_ARTIFACT_ID                 = android-instrumentation-test
 INSTRUMENTATION_DESCRIPTION                 = Extensions for instrumented Android tests with JUnit 5.
-INSTRUMENTATION_VERSION_NAME                = 0.1.1-SNAPSHOT
+INSTRUMENTATION_VERSION_NAME                = 0.1.1
 INSTRUMENTATION_MIN_SDK_VERSION             = 26
 
 # Dependency versions (plugins)
@@ -39,6 +39,12 @@ DCENDENTS_MAVEN_PLUGIN_VERSION              = 2.0
 GROOVY_VERSION                              = 3.0.0-alpha-1
 KOTLIN_VERSION                              = 1.2.0
 
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# A Note to my forgetful self:
+#
+# When updating these values, make sure
+# to always update the Travis CI .yml config, too
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 # Android Environment (common)
 COMPILE_SDK_VERSION                         = android-27
 BUILD_TOOLS_VERSION                         = 27.0.1
diff --git a/sample/build.gradle b/sample/build.gradle
index 3e35cd30..cd5f8862 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -14,17 +14,11 @@ apply plugin: "de.mannodermaus.android-junit5"
 apply plugin: "jacoco"
 
 android {
-  // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-  // A Note to my forgetful self:
-  //
-  // When updating these values, make sure
-  // to always update the Travis CI .yml config, too
-  // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   compileSdkVersion COMPILE_SDK_VERSION
 
   defaultConfig {
     applicationId "de.mannodermaus.junit5.sample"
-    minSdkVersion MIN_SDK_VERSION
+    minSdkVersion SAMPLE_MIN_SDK_VERSION
     targetSdkVersion TARGET_SDK_VERSION
     versionCode 1
     versionName "1.0"