From 05cc0f3207e5b4e84f364bf3d72cfc1f44280691 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sun, 14 Apr 2024 09:31:42 +0900 Subject: [PATCH] Allow autoconfiguration of Compose integration via the plugin It needs to detect an androidx.compose artifact in the androidTestImplementation configuration for this to work, but from there it sets itself up automatically, just like the main libraries do when they see junit-jupiter-api --- README.md.template | 44 ++++++-- plugin/CHANGELOG.md | 2 + plugin/android-junit5/build.gradle.kts | 1 + .../junit5/internal/configureJUnit5.kt | 17 ++- .../junit5/internal/extensions/ProjectExt.kt | 19 ++++ .../src/main/templates/Libraries.kt | 1 + .../plugin/InstrumentationSupportTests.kt | 100 ++++++++++++++---- .../gradle/plugins/junit5/util/GradleTruth.kt | 68 ++++++++++++ 8 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/GradleTruth.kt diff --git a/README.md.template b/README.md.template index ceff9701..dfeea43d 100644 --- a/README.md.template +++ b/README.md.template @@ -149,8 +149,8 @@ Can you think of more? Let's discuss in the issues section! Kotlin ```kotlin - dependencies { - androidTestImplementation("de.mannodermaus.junit5:android-test-extensions:${instrumentationVersion}") + junitPlatform { + instrumentationTests.includeExtensions.set(true) } ``` @@ -159,8 +159,8 @@ Can you think of more? Let's discuss in the issues section! Groovy ```groovy - dependencies { - androidTestImplementation "de.mannodermaus.junit5:android-test-extensions:${instrumentationVersion}" + junitPlatform { + instrumentationTests.includeExtensions.set(true) } ``` @@ -168,15 +168,16 @@ Can you think of more? Let's discuss in the issues section! ### Jetpack Compose To test `@Composable` functions on device with JUnit 5, first enable support for instrumentation tests as described above. -Then, add the integration library for Jetpack Compose to the `androidTestImplementation` configuration: +Then, add the Compose test dependency to your `androidTestImplementation` configuration +and the plugin will autoconfigure JUnit 5 Compose support for you!
Kotlin ```kotlin dependencies { - // Test extension & transitive dependencies - androidTestImplementation("de.mannodermaus.junit5:android-test-compose:${instrumentationVersion}") + // Compose test framework + androidTestImplementation("androidx.compose.ui:ui-test-android:$compose_version") // Needed for createComposeExtension() and createAndroidComposeExtension() debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") @@ -189,8 +190,8 @@ Then, add the integration library for Jetpack Compose to the `androidTestImpleme ```groovy dependencies { - // Test extension & transitive dependencies - androidTestImplementation "de.mannodermaus.junit5:android-test-compose:${instrumentationVersion}" + // Compose test framework + androidTestImplementation "androidx.compose.ui:ui-test-android:$compose_version" // Needed for createComposeExtension() and createAndroidComposeExtension() debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" @@ -200,6 +201,31 @@ Then, add the integration library for Jetpack Compose to the `androidTestImpleme [The wiki][wiki-home] includes a section on how to test your Composables with JUnit 5. +### Override the version of instrumentation test libraries + +By default, the plugin will make sure to use a compatible version of the instrumentation test libraries +when it sets up the artifacts automatically. However, it is possible to choose a custom version instead via its DSL: + +
+ Kotlin + + ```kotlin + junitPlatform { + instrumentationTests.version.set("${instrumentationVersion}") + } + ``` +
+ +
+ Groovy + + ```groovy + junitPlatform { + instrumentationTests.version.set("${instrumentationVersion}") + } + ``` +
+ ## Official Support At this time, Google hasn't shared any immediate plans to bring first-party support for JUnit 5 to Android. The following list is an aggregation of pending feature requests: diff --git a/plugin/CHANGELOG.md b/plugin/CHANGELOG.md index 491fcbb3..7e5167d7 100644 --- a/plugin/CHANGELOG.md +++ b/plugin/CHANGELOG.md @@ -7,6 +7,8 @@ Change Log - Allow overriding the version of the instrumentation libraries applied with the plugin - Update Jacoco & instrumentation test DSLs of the plugin to use Gradle Providers for their input parameters (e.g. `instrumentationTests.enabled.set(true)` instead of `instrumentationTests.enabled = true`) - Removed deprecated `integrityCheckEnabled` flag from the plugin DSL's instrumentation test options +- Allow opt-in usage of extension library via the plugin's DSL +- Allow autoconfiguration of compose library if Compose is used in the androidTest dependency list ## 1.10.0.0 (2023-11-05) - JUnit 5.10.0 diff --git a/plugin/android-junit5/build.gradle.kts b/plugin/android-junit5/build.gradle.kts index caba37bc..fed0c14d 100644 --- a/plugin/android-junit5/build.gradle.kts +++ b/plugin/android-junit5/build.gradle.kts @@ -88,6 +88,7 @@ val versionClassTask = tasks.register("createVersionClass") { mapOf( "tokens" to mapOf( "INSTRUMENTATION_GROUP" to Artifacts.Instrumentation.groupId, + "INSTRUMENTATION_COMPOSE" to Artifacts.Instrumentation.Compose.artifactId, "INSTRUMENTATION_CORE" to Artifacts.Instrumentation.Core.artifactId, "INSTRUMENTATION_EXTENSIONS" to Artifacts.Instrumentation.Extensions.artifactId, "INSTRUMENTATION_RUNNER" to Artifacts.Instrumentation.Runner.artifactId, diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt index 42bca48e..144e33ac 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/configureJUnit5.kt @@ -14,12 +14,16 @@ import de.mannodermaus.gradle.plugins.junit5.internal.config.PluginConfig import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android import de.mannodermaus.gradle.plugins.junit5.internal.extensions.getAsList import de.mannodermaus.gradle.plugins.junit5.internal.extensions.getTaskName +import de.mannodermaus.gradle.plugins.junit5.internal.extensions.hasDependency import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junit5Warn import de.mannodermaus.gradle.plugins.junit5.internal.extensions.namedOrNull +import de.mannodermaus.gradle.plugins.junit5.internal.extensions.usesComposeIn +import de.mannodermaus.gradle.plugins.junit5.internal.extensions.usesJUnitJupiterIn import de.mannodermaus.gradle.plugins.junit5.internal.utils.excludedPackagingOptions import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5JacocoReport import de.mannodermaus.gradle.plugins.junit5.tasks.AndroidJUnit5WriteFilters import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency import org.gradle.api.tasks.testing.Test internal fun configureJUnit5( @@ -109,11 +113,7 @@ private fun AndroidJUnitPlatformExtension.prepareInstrumentationTests(project: P if (!instrumentationTests.enabled.get()) return // Automatically configure instrumentation tests when JUnit 5 is detected in that configuration - val hasJupiterApi = project.configurations - .getByName("androidTestImplementation") - .dependencies - .any { it.group == "org.junit.jupiter" && it.name == "junit-jupiter-api" } - if (!hasJupiterApi) return + if (!project.usesJUnitJupiterIn("androidTestImplementation")) return // Attach the JUnit 5 RunnerBuilder to the list, unless it's already added val runnerBuilders = android.defaultConfig.testInstrumentationRunnerArguments.getAsList("runnerBuilder") @@ -141,6 +141,13 @@ private fun AndroidJUnitPlatformExtension.prepareInstrumentationTests(project: P "${Libraries.instrumentationExtensions}:$version" ) } + + if (project.usesComposeIn("androidTestImplementation")) { + project.dependencies.add( + "androidTestImplementation", + "${Libraries.instrumentationCompose}:$version" + ) + } } private fun AndroidJUnitPlatformExtension.configureUnitTests(project: Project, variant: Variant) { diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt index dd1b92ba..347b6cd8 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/internal/extensions/ProjectExt.kt @@ -5,6 +5,7 @@ import com.android.build.gradle.BasePlugin import de.mannodermaus.gradle.plugins.junit5.dsl.AndroidJUnitPlatformExtension import de.mannodermaus.gradle.plugins.junit5.internal.config.EXTENSION_NAME import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency import java.util.concurrent.atomic.AtomicBoolean import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -35,3 +36,21 @@ internal fun Project.whenAndroidPluginAdded(block: (BasePlugin) -> Unit) { } } } + +internal fun Project.hasDependency(configurationName: String, matching: (Dependency) -> Boolean): Boolean { + val configuration = project.configurations.getByName(configurationName) + + return configuration.dependencies.any(matching) +} + +internal fun Project.usesJUnitJupiterIn(configurationName: String): Boolean { + return project.hasDependency(configurationName) { + it.group == "org.junit.jupiter" && it.name == "junit-jupiter-api" + } +} + +internal fun Project.usesComposeIn(configurationName: String): Boolean { + return project.hasDependency(configurationName) { + it.group?.startsWith("androidx.compose") ?: false + } +} diff --git a/plugin/android-junit5/src/main/templates/Libraries.kt b/plugin/android-junit5/src/main/templates/Libraries.kt index 6986fe57..b992a9b2 100644 --- a/plugin/android-junit5/src/main/templates/Libraries.kt +++ b/plugin/android-junit5/src/main/templates/Libraries.kt @@ -2,6 +2,7 @@ package de.mannodermaus internal object Libraries { const val instrumentationVersion = "@INSTRUMENTATION_VERSION@" + const val instrumentationCompose = "@INSTRUMENTATION_GROUP@:@INSTRUMENTATION_COMPOSE@" const val instrumentationCore = "@INSTRUMENTATION_GROUP@:@INSTRUMENTATION_CORE@" const val instrumentationExtensions = "@INSTRUMENTATION_GROUP@:@INSTRUMENTATION_EXTENSIONS@" const val instrumentationRunner = "@INSTRUMENTATION_GROUP@:@INSTRUMENTATION_RUNNER@" diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt index 95031bdf..3baaa072 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/InstrumentationSupportTests.kt @@ -5,6 +5,7 @@ import de.mannodermaus.Libraries import de.mannodermaus.gradle.plugins.junit5.internal.config.ANDROID_JUNIT5_RUNNER_BUILDER_CLASS import de.mannodermaus.gradle.plugins.junit5.internal.extensions.android import de.mannodermaus.gradle.plugins.junit5.internal.extensions.junitPlatform +import de.mannodermaus.gradle.plugins.junit5.util.assertThat import de.mannodermaus.gradle.plugins.junit5.util.evaluate import org.gradle.api.Project import org.junit.jupiter.api.BeforeEach @@ -67,14 +68,15 @@ class InstrumentationSupportTests { /* Dependencies */ @Test - fun `add the dependencies`() { + fun `add only the main dependencies`() { project.addJUnitJupiterApi() project.evaluate() - assertThat(project.dependencyNamed("androidTestImplementation", "android-test-core")) - .isEqualTo("${Libraries.instrumentationCore}:${Libraries.instrumentationVersion}") - assertThat(project.dependencyNamed("androidTestRuntimeOnly", "android-test-runner")) - .isEqualTo("${Libraries.instrumentationRunner}:${Libraries.instrumentationVersion}") + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary()) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary()) + + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(extensionsLibrary()) + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(composeLibrary()) } @Test @@ -83,8 +85,8 @@ class InstrumentationSupportTests { project.junitPlatform.instrumentationTests.version.set("1.3.3.7") project.evaluate() - assertThat(project.dependencyNamed("androidTestImplementation", "android-test-core")).endsWith("1.3.3.7") - assertThat(project.dependencyNamed("androidTestRuntimeOnly", "android-test-runner")).endsWith("1.3.3.7") + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary("1.3.3.7")) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary("1.3.3.7")) } @Test @@ -96,8 +98,8 @@ class InstrumentationSupportTests { project.dependencies.add("androidTestRuntimeOnly", addedRunner) project.evaluate() - assertThat(project.dependencyNamed("androidTestImplementation", "android-test-core")).isEqualTo(addedCore) - assertThat(project.dependencyNamed("androidTestRuntimeOnly", "android-test-runner")).isEqualTo(addedRunner) + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary("0.1.3.3.7")) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary("0.1.3.3.7")) } @Test @@ -106,16 +108,61 @@ class InstrumentationSupportTests { project.junitPlatform.instrumentationTests.enabled.set(false) project.evaluate() - assertThat(project.dependencyNamed("androidTestImplementation", "android-test-core")).isNull() - assertThat(project.dependencyNamed("androidTestRuntimeOnly", "android-test-runner")).isNull() + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(coreLibrary(null)) + assertThat(project).configuration("androidTestRuntimeOnly").doesNotHaveDependency(runnerLibrary(null)) } @Test fun `do not add the dependencies when Jupiter is not added`() { project.evaluate() - assertThat(project.dependencyNamed("androidTestImplementation", "android-test-core")).isNull() - assertThat(project.dependencyNamed("androidTestRuntimeOnly", "android-test-runner")).isNull() + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(coreLibrary(null)) + assertThat(project).configuration("androidTestRuntimeOnly").doesNotHaveDependency(runnerLibrary(null)) + } + + @Test + fun `do not add the dependencies when Jupiter is not added, even if extension is configured to be added`() { + project.junitPlatform.instrumentationTests.includeExtensions.set(true) + project.evaluate() + + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(coreLibrary(null)) + assertThat(project).configuration("androidTestImplementation").doesNotHaveDependency(extensionsLibrary(null)) + assertThat(project).configuration("androidTestRuntimeOnly").doesNotHaveDependency(runnerLibrary(null)) + } + + @Test + fun `add the extension library if configured`() { + project.addJUnitJupiterApi() + project.junitPlatform.instrumentationTests.includeExtensions.set(true) + project.evaluate() + + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary()) + assertThat(project).configuration("androidTestImplementation").hasDependency(extensionsLibrary()) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary()) + } + + @Test + fun `add the compose library if configured`() { + project.addJUnitJupiterApi() + project.addCompose() + project.evaluate() + + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary()) + assertThat(project).configuration("androidTestImplementation").hasDependency(composeLibrary()) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary()) + } + + @Test + fun `add the extensions and compose libraries if configured`() { + project.addJUnitJupiterApi() + project.addCompose() + project.junitPlatform.instrumentationTests.includeExtensions.set(true) + project.evaluate() + + assertThat(project).configuration("androidTestImplementation").hasDependency(coreLibrary()) + assertThat(project).configuration("androidTestImplementation").hasDependency(composeLibrary()) + assertThat(project).configuration("androidTestImplementation").hasDependency(extensionsLibrary()) + assertThat(project).configuration("androidTestRuntimeOnly").hasDependency(runnerLibrary()) } /* Private */ @@ -124,10 +171,27 @@ class InstrumentationSupportTests { dependencies.add("androidTestImplementation", "org.junit.jupiter:junit-jupiter-api:+") } - private fun Project.dependencyNamed(configurationName: String, name: String): String? { - return configurations.getByName(configurationName) - .dependencies - .firstOrNull { it.name == name } - ?.run { "$group:$name:$version" } + private fun Project.addCompose() { + dependencies.add("androidTestImplementation", "androidx.compose.ui:ui-test-android:+") + } + + private fun composeLibrary(withVersion: String? = Libraries.instrumentationVersion) = + library(Libraries.instrumentationCompose, withVersion) + + private fun coreLibrary(withVersion: String? = Libraries.instrumentationVersion) = + library(Libraries.instrumentationCore, withVersion) + + private fun extensionsLibrary(withVersion: String? = Libraries.instrumentationVersion) = + library(Libraries.instrumentationExtensions, withVersion) + + private fun runnerLibrary(withVersion: String? = Libraries.instrumentationVersion) = + library(Libraries.instrumentationRunner, withVersion) + + private fun library(artifactId: String, version: String?) = buildString { + append(artifactId) + if (version != null) { + append(':') + append(version) + } } } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/GradleTruth.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/GradleTruth.kt new file mode 100644 index 00000000..f5742239 --- /dev/null +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/util/GradleTruth.kt @@ -0,0 +1,68 @@ +package de.mannodermaus.gradle.plugins.junit5.util + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertWithMessage +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration + +/* Methods */ + +fun assertThat(actual: Project): ProjectSubject = + Truth.assertAbout(::ProjectSubject).that(actual) + +/* Types */ + +class ProjectSubject( + metadata: FailureMetadata, + private val actual: Project?, +) : Subject(metadata, actual) { + + fun configuration(name: String): ConfigurationSubject = check("configuration()") + .about(::ConfigurationSubject) + .that(actual?.configurations?.getByName(name)) +} + +class ConfigurationSubject( + metadata: FailureMetadata, + private val actual: Configuration?, +) : Subject(metadata, actual) { + private val dependencyNames by lazy { + actual?.dependencies + ?.map { "${it.group}:${it.name}:${it.version}" } + .orEmpty() + } + + fun hasDependency(notation: String) { + containsDependency(notation, expectExists = true) + } + + fun doesNotHaveDependency(notation: String) { + containsDependency(notation, expectExists = false) + } + + /* Private */ + + private fun containsDependency(notation: String, expectExists: Boolean) { + // If the expected dependency has a version component, + // include it in the check. Otherwise, check for the existence + // of _any_ version for the dependency in question + val notationIncludesVersion = notation.count { it == ':' } > 1 + val hasMatch = if (notationIncludesVersion) { + notation in dependencyNames + } else { + dependencyNames.any { it.startsWith("$notation:") } + } + + val messagePrefix = if (expectExists) { + "Expected to have a dependency on '$notation' in configuration '${actual?.name}', but did not." + } else { + "Expected not to have a dependency on '$notation' in configuration '${actual?.name}', but did." + } + + assertWithMessage( + "$messagePrefix\nDependencies in this configuration: $dependencyNames" + ).that(hasMatch).isEqualTo(expectExists) + } +}