Skip to content

Commit be10b22

Browse files
authored
Prevent test methods incorrectly defined as Kotlin top-level functions from being considered for test execution (#325)
This won't work, as JUnit 5 doesn't actually run these methods. However, if a runner is created for the class, then there is a mismatch between Android's expectations and JUnit 5's deliverables. This messes up Android's internal test counting, causing issues like "Expected N+1 tests, received N"
1 parent bdecf1b commit be10b22

File tree

4 files changed

+92
-1
lines changed

4 files changed

+92
-1
lines changed

instrumentation/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Change Log
66
- Fix inheritance hierarchy of `ComposeExtension` to avoid false-positive warning regarding `@RegisterExtension` (#318)
77
- Improve parallel test execution for Android instrumentation tests
88
- Fix invalid naming of dynamic tests when executing only a singular test method from the IDE (#317)
9+
- Prevent test methods incorrectly defined as Kotlin top-level functions from messing up Android's internal test counting, causing issues like "Expected N+1 tests, received N" (#316)
910

1011
## 1.4.0 (2023-11-05)
1112

instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/extensions/ClassExt.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package de.mannodermaus.junit5.internal.extensions
33
import android.util.Log
44
import de.mannodermaus.junit5.internal.LOG_TAG
55
import java.lang.reflect.Method
6+
import java.lang.reflect.Modifier
67

78
private val jupiterTestAnnotations = listOf(
89
"org.junit.jupiter.api.Test",
@@ -38,6 +39,12 @@ private fun Class<*>.jupiterTestMethods(includeInherited: Boolean): Set<Method>
3839

3940
private fun Array<Method>.filterAnnotatedByJUnitJupiter(): List<Method> =
4041
filter { method ->
42+
// The method must not be static...
43+
if (method.isStatic) return@filter false
44+
45+
// ...and have at least one of the recognized JUnit 5 annotations
4146
val names = method.declaredAnnotations.map { it.annotationClass.qualifiedName }
42-
jupiterTestAnnotations.any { it in names }
47+
jupiterTestAnnotations.any(names::contains)
4348
}
49+
50+
private val Method.isStatic get() = Modifier.isStatic(modifiers)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package de.mannodermaus.junit5
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.junit.jupiter.api.Test
5+
import org.junit.jupiter.params.ParameterizedTest
6+
import org.junit.jupiter.params.provider.ValueSource
7+
8+
class AndroidJUnit5BuilderTests {
9+
10+
private val builder = AndroidJUnit5Builder()
11+
12+
@Test
13+
fun `no runner is created if class only contains top-level test methods`() {
14+
// In Kotlin, a 'Kt'-suffixed class of top-level functions cannot be referenced
15+
// via the ::class syntax, so construct a reference to the class directly
16+
val cls = Class.forName(javaClass.packageName + ".TestClassesKt")
17+
18+
// Top-level tests should be discarded, so no runner must be created for this class
19+
runTest(cls, expectSuccess = false)
20+
}
21+
22+
@ValueSource(
23+
classes = [
24+
HasTest::class,
25+
HasRepeatedTest::class,
26+
HasTestFactory::class,
27+
HasTestTemplate::class,
28+
HasParameterizedTest::class,
29+
HasInnerClassWithTest::class,
30+
HasTaggedTest::class,
31+
HasInheritedTestsFromClass::class,
32+
HasInheritedTestsFromInterface::class,
33+
]
34+
)
35+
@ParameterizedTest
36+
fun `runner is created correctly for classes with valid jupiter test methods`(cls: Class<*>) =
37+
runTest(cls, expectSuccess = true)
38+
39+
@ValueSource(
40+
classes = [
41+
DoesntHaveTestMethods::class,
42+
HasJUnit4Tests::class,
43+
kotlin.time.Duration::class,
44+
]
45+
)
46+
@ParameterizedTest
47+
fun `no runner is created if class has no jupiter test methods`(cls: Class<*>) =
48+
runTest(cls, expectSuccess = false)
49+
50+
/* Private */
51+
52+
private fun runTest(cls: Class<*>, expectSuccess: Boolean) {
53+
val runner = builder.runnerForClass(cls)
54+
if (expectSuccess) {
55+
assertThat(runner).isNotNull()
56+
} else {
57+
assertThat(runner).isNull()
58+
}
59+
}
60+
}

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

+23
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.DynamicTest.dynamicTest
55
import org.junit.jupiter.api.extension.*
66
import org.junit.jupiter.params.ParameterizedTest
77
import org.junit.jupiter.params.provider.CsvSource
8+
import org.junit.jupiter.params.provider.ValueSource
89
import java.util.stream.Stream
910

1011
class DoesntHaveTestMethods
@@ -101,3 +102,25 @@ class HasInheritedTestsFromClass : AbstractTestClass() {
101102
}
102103

103104
class HasInheritedTestsFromInterface : AbstractTestInterface
105+
106+
// These tests should not be acknowledged,
107+
// as classes with legacy tests & top-level tests
108+
// are unsupported by JUnit 5
109+
110+
class HasJUnit4Tests {
111+
@org.junit.Test
112+
fun method() {}
113+
}
114+
115+
@RepeatedTest(2)
116+
fun topLevelRepeatedTest(unused: RepetitionInfo) {}
117+
118+
@ValueSource(strings = ["a", "b"])
119+
@ParameterizedTest
120+
fun topLevelParameterizedTest(unused: String) {}
121+
122+
@TestTemplate
123+
fun topLevelTestTemplate() {}
124+
125+
@TestFactory
126+
fun topLevelTestFactory(): Stream<DynamicNode> = Stream.empty()

0 commit comments

Comments
 (0)