Skip to content

Commit

Permalink
Allow autoconfiguration of Compose integration via the plugin (#327)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mannodermaus authored Apr 14, 2024
1 parent d082fce commit 38ef72c
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 32 deletions.
44 changes: 35 additions & 9 deletions README.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ Can you think of more? Let's discuss in the issues section!
<summary>Kotlin</summary>

```kotlin
dependencies {
androidTestImplementation("de.mannodermaus.junit5:android-test-extensions:${instrumentationVersion}")
junitPlatform {
instrumentationTests.includeExtensions.set(true)
}
```
</details>
Expand All @@ -159,24 +159,25 @@ Can you think of more? Let's discuss in the issues section!
<summary>Groovy</summary>

```groovy
dependencies {
androidTestImplementation "de.mannodermaus.junit5:android-test-extensions:${instrumentationVersion}"
junitPlatform {
instrumentationTests.includeExtensions.set(true)
}
```
</details>

### 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!

<details open>
<summary>Kotlin</summary>

```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")
Expand All @@ -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"
Expand All @@ -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:

<details open>
<summary>Kotlin</summary>

```kotlin
junitPlatform {
instrumentationTests.version.set("${instrumentationVersion}")
}
```
</details>

<details>
<summary>Groovy</summary>

```groovy
junitPlatform {
instrumentationTests.version.set("${instrumentationVersion}")
}
```
</details>

## 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:
Expand Down
2 changes: 2 additions & 0 deletions plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions plugin/android-junit5/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ val versionClassTask = tasks.register<Copy>("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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions plugin/android-junit5/src/main/templates/Libraries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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@"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 */
Expand All @@ -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)
}
}
}
Loading

0 comments on commit 38ef72c

Please sign in to comment.