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