Skip to content

Commit 1d17eb7

Browse files
author
Marcel Schnelle
authored
Show JUnit 5 instrumentation tests as ignored on older devices (#154)
Adds a dummy implementation of JUnit 5's instrumentation runner. This, in conjunction with an overrideLibrary statement in the AndroidManifest, allows users to write JUnit 5 instrumentation tests & still execute their suite on lower API levels, without a build failure. At runtime, JUnit 5 tests will be marked as ignored on these devices. * Update tests for instrumentation runner, adding support for remaining Test annotations Now, the plugin can also detect TestTemplate & RepeatedTest properly
1 parent 4bc1ff4 commit 1d17eb7

File tree

10 files changed

+173
-107
lines changed

10 files changed

+173
-107
lines changed

instrumentation/runner/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ dependencies {
101101
compileOnly(Libs.junit_jupiter_params)
102102
compileOnly(Libs.junit_platform_runner)
103103

104-
testImplementation(Libs.assertj_core)
104+
testImplementation(Libs.truth)
105105
testImplementation(Libs.mockito_core)
106106
testImplementation(Libs.junit_jupiter_api)
107107
testImplementation(Libs.junit_jupiter_params)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package de.mannodermaus.junit5
2+
3+
import android.util.Log
4+
import org.junit.runner.Description
5+
import org.junit.runner.Runner
6+
import org.junit.runner.notification.RunNotifier
7+
8+
/**
9+
* Created by Marcel Schnelle on 2019-03-16.
10+
*/
11+
class DummyJUnit5(private val testClass: Class<*>) : Runner() {
12+
13+
private val testMethods = testClass.jupiterTestMethods()
14+
15+
override fun run(notifier: RunNotifier) {
16+
Log.w(LOG_TAG, "JUnit 5 is not supported on this device. All Jupiter tests will be disabled.")
17+
18+
for (testMethod in testMethods) {
19+
val description = Description.createTestDescription(testClass, testMethod.name)
20+
notifier.fireTestIgnored(description)
21+
}
22+
}
23+
24+
override fun getDescription(): Description = Description.createSuiteDescription(testClass)
25+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package de.mannodermaus.junit5
2+
3+
import android.util.Log
4+
import java.lang.reflect.Method
5+
6+
/**
7+
* Created by Marcel Schnelle on 2019-03-16.
8+
*/
9+
10+
const val LOG_TAG = "AndroidJUnit5"
11+
private val jupiterTestAnnotations = listOf(
12+
"org.junit.jupiter.api.Test",
13+
"org.junit.jupiter.api.TestFactory",
14+
"org.junit.jupiter.api.RepeatedTest",
15+
"org.junit.jupiter.api.TestTemplate",
16+
"org.junit.jupiter.params.ParameterizedTest")
17+
18+
fun Class<*>.jupiterTestMethods(): List<Method> {
19+
val allJupiterMethods = mutableListOf<Method>()
20+
try {
21+
22+
// Check each method in the Class for the presence
23+
// of the well-known list of JUnit Jupiter annotations
24+
allJupiterMethods += declaredMethods.filter { method ->
25+
val annotationClassNames = method.declaredAnnotations.map { it.annotationClass.qualifiedName }
26+
jupiterTestAnnotations.firstOrNull { annotation ->
27+
annotationClassNames.contains(annotation)
28+
} != null
29+
}
30+
31+
// Recursively check inner classes as well
32+
declaredClasses.forEach { inner ->
33+
allJupiterMethods += inner.jupiterTestMethods()
34+
}
35+
36+
} catch (t: Throwable) {
37+
Log.w(LOG_TAG, "${t.javaClass.name} in 'hasJupiterTestMethods()' for $name", t)
38+
}
39+
40+
return allJupiterMethods
41+
}

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/Runner.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.mannodermaus.junit5
22

3+
import android.os.Build
34
import org.junit.platform.runner.JUnitPlatform
45
import org.junit.runner.Runner
56

@@ -18,5 +19,14 @@ internal class AndroidJUnit5(klass: Class<*>) : JUnitPlatform(klass)
1819

1920
/**
2021
* Since we can't reference AndroidJUnit5 directly, use this factory for instantiation.
22+
*
23+
* On API 26 and above, delegate to the real implementation to drive JUnit 5 tests.
24+
* Below that however, they wouldn't work; for this case, delegate a dummy runner
25+
* which will highlight these tests as ignored.
2126
*/
22-
internal fun createJUnit5Runner(klass: Class<*>): Runner = AndroidJUnit5(klass)
27+
internal fun createJUnit5Runner(klass: Class<*>): Runner =
28+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
29+
AndroidJUnit5(klass)
30+
} else {
31+
DummyJUnit5(klass)
32+
}

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/RunnerBuilder.kt

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,6 @@ import android.util.Log
44
import org.junit.runner.Runner
55
import org.junit.runners.model.RunnerBuilder
66

7-
/* Constants */
8-
9-
private const val LOG_TAG = "AndroidJUnit5"
10-
private val jupiterTestAnnotations = listOf(
11-
"org.junit.jupiter.api.Test",
12-
"org.junit.jupiter.api.TestFactory",
13-
"org.junit.jupiter.params.ParameterizedTest")
14-
15-
/* Types */
16-
177
/**
188
* Custom RunnerBuilder hooked into the main Test Instrumentation Runner
199
* provided by the Android Test Support Library, which allows to run
@@ -63,9 +53,10 @@ class AndroidJUnit5Builder : RunnerBuilder() {
6353
return null
6454
}
6555

66-
if (!testClass.hasJupiterTestMethods()) {
56+
if (testClass.jupiterTestMethods().isEmpty()) {
6757
return null
6858
}
59+
6960
return createJUnit5Runner(testClass)
7061

7162
} catch (e: NoClassDefFoundError) {
@@ -79,36 +70,4 @@ class AndroidJUnit5Builder : RunnerBuilder() {
7970
throw e
8071
}
8172
}
82-
83-
/* Extension Functions */
84-
85-
private fun Class<*>.hasJupiterTestMethods(): Boolean {
86-
try {
87-
// Check each method in the Class for the presence
88-
// of the well-known list of JUnit Jupiter annotations
89-
val testMethod = declaredMethods.firstOrNull { method ->
90-
val annotationClassNames = method.declaredAnnotations.map { it.annotationClass.qualifiedName }
91-
jupiterTestAnnotations.firstOrNull { annotation ->
92-
annotationClassNames.contains(annotation)
93-
} != null
94-
}
95-
96-
if (testMethod != null) {
97-
Log.i(LOG_TAG, "Jupiter Test Class detected: ${this.name}")
98-
return true
99-
}
100-
101-
// Recursively check inner classes as well
102-
declaredClasses.forEach { inner ->
103-
if (inner.hasJupiterTestMethods()) {
104-
return true
105-
}
106-
}
107-
108-
} catch (t: Throwable) {
109-
Log.w(LOG_TAG, "${t.javaClass.name} in 'hasJupiterTestMethods()' for $name", t)
110-
}
111-
112-
return false
113-
}
11473
}

instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/test/Classes.kt

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package de.mannodermaus.junit5.test
22

3+
import org.junit.jupiter.api.*
34
import org.junit.jupiter.api.DynamicTest.dynamicTest
4-
import org.junit.jupiter.api.Nested
5-
import org.junit.jupiter.api.Test
6-
import org.junit.jupiter.api.TestFactory
5+
import org.junit.jupiter.api.extension.*
76
import org.junit.jupiter.params.ParameterizedTest
87
import org.junit.jupiter.params.provider.CsvSource
8+
import java.util.stream.Stream
99

1010
class DoesntHaveTestMethods
1111

@@ -15,6 +15,12 @@ class HasTest {
1515
}
1616
}
1717

18+
class HasRepeatedTest {
19+
@RepeatedTest(5)
20+
fun method(info: RepetitionInfo) {
21+
}
22+
}
23+
1824
class HasTestFactory {
1925
@TestFactory
2026
fun method() = listOf(
@@ -23,6 +29,35 @@ class HasTestFactory {
2329
)
2430
}
2531

32+
class HasTestTemplate {
33+
@TestTemplate
34+
@ExtendWith(ExampleInvocationContextProvider::class)
35+
fun method(param: String) {
36+
}
37+
38+
class ExampleInvocationContextProvider : TestTemplateInvocationContextProvider {
39+
override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream<TestTemplateInvocationContext> =
40+
listOf("param1", "param2")
41+
.map(this::context)
42+
.stream()
43+
44+
override fun supportsTestTemplate(context: ExtensionContext) = true
45+
46+
private fun context(param: String): TestTemplateInvocationContext =
47+
object : TestTemplateInvocationContext {
48+
override fun getAdditionalExtensions() = listOf(
49+
object : ParameterResolver {
50+
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext) =
51+
parameterContext.parameter.type == String::class.java
52+
53+
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext) =
54+
param
55+
}
56+
)
57+
}
58+
}
59+
}
60+
2661
class HasParameterizedTest {
2762
@ParameterizedTest
2863
@CsvSource("a", "b")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@file:Suppress("unused")
2+
3+
package de.mannodermaus.junit5.test
4+
5+
import com.google.common.truth.Truth.assertThat
6+
import de.mannodermaus.junit5.AndroidJUnit5
7+
import de.mannodermaus.junit5.jupiterTestMethods
8+
import org.junit.jupiter.api.DisplayName
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import org.junit.jupiter.params.provider.Arguments
11+
import org.junit.jupiter.params.provider.MethodSource
12+
import org.junit.runner.notification.RunNotifier
13+
14+
class ExtensionsTests {
15+
16+
@ParameterizedTest
17+
@MethodSource("jupiterTestMethods")
18+
@DisplayName("jupiterTestMethods() has correct values & will execute expected number of tests")
19+
fun jupiterTestMethods(klass: Class<*>, expectExecutedTests: Int) {
20+
val methods = klass.jupiterTestMethods()
21+
if (expectExecutedTests == 0) {
22+
assertThat(methods).isEmpty()
23+
} else {
24+
assertThat(methods).isNotEmpty()
25+
26+
// Verify number of executed test cases as well
27+
val notifier = RunNotifier()
28+
val listener = CountingRunListener()
29+
notifier.addListener(listener)
30+
AndroidJUnit5(klass).run(notifier)
31+
32+
assertThat(listener.count())
33+
.named("Executed ${listener.count()} instead of $expectExecutedTests tests: '${listener.methodNames()}'")
34+
.isEqualTo(expectExecutedTests)
35+
}
36+
}
37+
38+
companion object {
39+
@JvmStatic
40+
fun jupiterTestMethods() = listOf(
41+
Arguments.of(DoesntHaveTestMethods::class.java, 0),
42+
Arguments.of(HasTest::class.java, 1),
43+
Arguments.of(HasRepeatedTest::class.java, 5),
44+
Arguments.of(HasTestFactory::class.java, 2),
45+
Arguments.of(HasTestTemplate::class.java, 2),
46+
Arguments.of(HasParameterizedTest::class.java, 2),
47+
Arguments.of(HasInnerClassWithTest::class.java, 1)
48+
)
49+
}
50+
}

instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/test/RunnerTests.kt

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<manifest xmlns:tools="http://schemas.android.com/tools"
2+
package="de.mannodermaus.junit5.test">
3+
4+
<uses-sdk tools:overrideLibrary="de.mannodermaus.junit5" />
5+
</manifest>

instrumentation/sample/src/androidTestExperimental/kotlin/de/mannodermaus/sample/TestRunningOnJUnit5.kt renamed to instrumentation/sample/src/androidTest/kotlin/de/mannodermaus/sample/TestRunningOnJUnit5.kt

File renamed without changes.

0 commit comments

Comments
 (0)