Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow autoconfiguration of Compose integration via the plugin #327

Merged
merged 1 commit into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading