在静态成员中展平 JUnit 测试 类
Flatten JUnit tests in static member classes
对于我的一个项目,我使用 JUnit 5 来测试反射代码,这需要大量 classes 用于测试用例。将它们全部放在一个范围内并试图智能地命名它们几乎是不可能的,所以我希望将测试方法和被测试的类型都放在静态成员 class 中。这样做将允许我在每个测试中重用 X
或 Y
等名称,并使被测试的类型靠近测试它们的代码。 (成员 classes 必须是静态的,这样我才能添加接口)
如果我只是添加静态 classes 测试 运行 开箱即用,但在最终报告中我最终将所有成员 classes 单独列出,所以我希望能够 "flatten" 将它们全部合并到报告中的单个 class 中。
这是我想要实现的示例:(我实际上是在 Kotlin 中编写测试,但这是等效的 Java 代码)
class MethodTests {
static class WhateverName {
interface X {}
class Y implements X {}
@Test
void something_withInterfaceSupertype_shouldReturnThing() {
// ...
}
@Test
void other_withInterfaceSupertype_shouldReturnThing() {
// ...
}
}
static class WhateverOtherName {
interface X {
void m();
}
class Y implements X {
void m() {}
}
@Test
void something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing() {
// ...
}
}
// This might actually be even better, since I wouldn't need `WhateverName`
@Test // not valid, but maybe I could annotate the class instead of the method?
static class something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo {
interface X {}
class Y implements X {}
@Test
void runTest() {
// ...
}
}
}
目前 IDEA 中的测试报告最终结构如下:
- MethodTests
- someRoot_testHere
- MethodTests$WhateverName
- something_withInterfaceSupertype_shouldReturnThing
- other_withInterfaceSupertype_shouldReturnThing
- MethodTests$WhateverOtherName
- something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing
- MethodTests$something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo
- runTest
但我希望测试报告的结构如下:
- MethodTests
- someRoot_testHere
- something_withInterfaceSupertype_shouldReturnThing
- other_withInterfaceSupertype_shouldReturnThing
- something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing
- something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo
我尝试在成员 classes 上使用 @DisplayName
,但它只是导致报告中的名称重复。到目前为止,我想我可能想使用扩展,但经过一些研究后,我还没有找到任何方法来更改 class 测试在使用它们的报告中列出。
也许您可以重新组织输出文件和索引,或者使用 xsl/xslt 或其他形式的 post 处理来处理它们。另外,这个 article 可能很有趣。
经过更多挖掘,我几乎可以使用 dynamic tests:
实现我想要的
class MethodsTest {
@TestFactory
Iterator<DynamicTest> flat() {
return FlatTestScanner.scan(this);
}
@Test
void rootTest() {
}
@FlatTest
static class singleTestClass implements TestClass {
void run() {
// ...
}
}
static class Whatever {
@FlatTest
void multiTestClass_1() {
// ...
}
@FlatTest
void multiTestClass_2() {
// ...
}
}
}
最终的报告结构不是很完美,但它非常接近我想要的:
- MethodsTest
- flat()
- singleTestClass
- multiTestClass_1
- multiTestClass_2
- rootTest
这是实现这一目标的代码。它的工作原理是扫描所有已声明的 类 以查找带注释的方法并抓取任何本身带注释的方法,然后为它们创建动态测试,确保 specify their source URIs。它在 Kotlin 中,但需要一些工作才能翻译成 Java:
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.DynamicTest
import java.net.URI
/**
* Useful for having separate class scopes for tests without having fragmented reports.
*
* @see FlatTestScanner.scan
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class FlatTest
object FlatTestScanner {
/**
* Returns dynamic tests to run all the flat tests declared in the passed class. This currently only works with
* static inner classes.
*
* - Annotated functions in inner classes will be run
* - Annotated inner classes will have their `run()` methods run
*
* To use this create a method in the outer class annotated with [@TestFactory][org.junit.jupiter.api.TestFactory]
* and return the result of passing `this` to this method. This will return matches from superclasses as well.
*
* ```java
* @TestFactory
* Iterator<DynamicTest> flat() {
* return FlatTestScanner.scan(this)
* }
* ```
*/
@JvmStatic
fun scan(obj: Any): Iterator<DynamicTest> {
val classes = generateSequence<Class<*>>(obj.javaClass) { it.superclass }
.flatMap { it.declaredClasses.asSequence() }
.toList()
val testMethods = classes.asSequence()
.map { clazz ->
clazz to clazz.declaredMethods.filter { m -> m.isAnnotationPresent(FlatTest::class.java) }
}
.filter { (_, methods) -> methods.isNotEmpty() }
.flatMap { (clazz, methods) ->
val instance = clazz.newInstance()
methods.asSequence().map { m ->
val name = m.getAnnotation(DisplayName::class.java)?.value ?: m.name
m.isAccessible = true
DynamicTest.dynamicTest(name, URI("method:${clazz.canonicalName}#${m.name}")) {
try {
m.invoke(instance)
} catch(e: InvocationTargetException) {
e.cause?.also { throw it } // unwrap assertion failures
}
}
}
}
val testClasses = classes.asSequence()
.filter { it.isAnnotationPresent(FlatTest::class.java) }
.map {
val name = it.getAnnotation(DisplayName::class.java)?.value ?: it.simpleName
val instance = it.newInstance()
val method = it.getDeclaredMethod("run")
method.isAccessible = true
DynamicTest.dynamicTest(name, URI("method:${it.canonicalName}#run")) {
try {
method.invoke(instance)
} catch(e: InvocationTargetException) {
e.cause?.also { throw it } // unwrap assertion failures
}
}
}
return (testMethods + testClasses).iterator()
}
}
对于我的一个项目,我使用 JUnit 5 来测试反射代码,这需要大量 classes 用于测试用例。将它们全部放在一个范围内并试图智能地命名它们几乎是不可能的,所以我希望将测试方法和被测试的类型都放在静态成员 class 中。这样做将允许我在每个测试中重用 X
或 Y
等名称,并使被测试的类型靠近测试它们的代码。 (成员 classes 必须是静态的,这样我才能添加接口)
如果我只是添加静态 classes 测试 运行 开箱即用,但在最终报告中我最终将所有成员 classes 单独列出,所以我希望能够 "flatten" 将它们全部合并到报告中的单个 class 中。
这是我想要实现的示例:(我实际上是在 Kotlin 中编写测试,但这是等效的 Java 代码)
class MethodTests {
static class WhateverName {
interface X {}
class Y implements X {}
@Test
void something_withInterfaceSupertype_shouldReturnThing() {
// ...
}
@Test
void other_withInterfaceSupertype_shouldReturnThing() {
// ...
}
}
static class WhateverOtherName {
interface X {
void m();
}
class Y implements X {
void m() {}
}
@Test
void something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing() {
// ...
}
}
// This might actually be even better, since I wouldn't need `WhateverName`
@Test // not valid, but maybe I could annotate the class instead of the method?
static class something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo {
interface X {}
class Y implements X {}
@Test
void runTest() {
// ...
}
}
}
目前 IDEA 中的测试报告最终结构如下:
- MethodTests
- someRoot_testHere
- MethodTests$WhateverName
- something_withInterfaceSupertype_shouldReturnThing
- other_withInterfaceSupertype_shouldReturnThing
- MethodTests$WhateverOtherName
- something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing
- MethodTests$something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo
- runTest
但我希望测试报告的结构如下:
- MethodTests
- someRoot_testHere
- something_withInterfaceSupertype_shouldReturnThing
- other_withInterfaceSupertype_shouldReturnThing
- something_withInterfaceSupertype_andMethodImpl_shouldReturnOtherThing
- something_withInterfaceSupertype_shouldReturnThing_2ElectricBoogaloo
我尝试在成员 classes 上使用 @DisplayName
,但它只是导致报告中的名称重复。到目前为止,我想我可能想使用扩展,但经过一些研究后,我还没有找到任何方法来更改 class 测试在使用它们的报告中列出。
也许您可以重新组织输出文件和索引,或者使用 xsl/xslt 或其他形式的 post 处理来处理它们。另外,这个 article 可能很有趣。
经过更多挖掘,我几乎可以使用 dynamic tests:
实现我想要的class MethodsTest {
@TestFactory
Iterator<DynamicTest> flat() {
return FlatTestScanner.scan(this);
}
@Test
void rootTest() {
}
@FlatTest
static class singleTestClass implements TestClass {
void run() {
// ...
}
}
static class Whatever {
@FlatTest
void multiTestClass_1() {
// ...
}
@FlatTest
void multiTestClass_2() {
// ...
}
}
}
最终的报告结构不是很完美,但它非常接近我想要的:
- MethodsTest
- flat()
- singleTestClass
- multiTestClass_1
- multiTestClass_2
- rootTest
这是实现这一目标的代码。它的工作原理是扫描所有已声明的 类 以查找带注释的方法并抓取任何本身带注释的方法,然后为它们创建动态测试,确保 specify their source URIs。它在 Kotlin 中,但需要一些工作才能翻译成 Java:
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.DynamicTest
import java.net.URI
/**
* Useful for having separate class scopes for tests without having fragmented reports.
*
* @see FlatTestScanner.scan
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class FlatTest
object FlatTestScanner {
/**
* Returns dynamic tests to run all the flat tests declared in the passed class. This currently only works with
* static inner classes.
*
* - Annotated functions in inner classes will be run
* - Annotated inner classes will have their `run()` methods run
*
* To use this create a method in the outer class annotated with [@TestFactory][org.junit.jupiter.api.TestFactory]
* and return the result of passing `this` to this method. This will return matches from superclasses as well.
*
* ```java
* @TestFactory
* Iterator<DynamicTest> flat() {
* return FlatTestScanner.scan(this)
* }
* ```
*/
@JvmStatic
fun scan(obj: Any): Iterator<DynamicTest> {
val classes = generateSequence<Class<*>>(obj.javaClass) { it.superclass }
.flatMap { it.declaredClasses.asSequence() }
.toList()
val testMethods = classes.asSequence()
.map { clazz ->
clazz to clazz.declaredMethods.filter { m -> m.isAnnotationPresent(FlatTest::class.java) }
}
.filter { (_, methods) -> methods.isNotEmpty() }
.flatMap { (clazz, methods) ->
val instance = clazz.newInstance()
methods.asSequence().map { m ->
val name = m.getAnnotation(DisplayName::class.java)?.value ?: m.name
m.isAccessible = true
DynamicTest.dynamicTest(name, URI("method:${clazz.canonicalName}#${m.name}")) {
try {
m.invoke(instance)
} catch(e: InvocationTargetException) {
e.cause?.also { throw it } // unwrap assertion failures
}
}
}
}
val testClasses = classes.asSequence()
.filter { it.isAnnotationPresent(FlatTest::class.java) }
.map {
val name = it.getAnnotation(DisplayName::class.java)?.value ?: it.simpleName
val instance = it.newInstance()
val method = it.getDeclaredMethod("run")
method.isAccessible = true
DynamicTest.dynamicTest(name, URI("method:${it.canonicalName}#run")) {
try {
method.invoke(instance)
} catch(e: InvocationTargetException) {
e.cause?.also { throw it } // unwrap assertion failures
}
}
}
return (testMethods + testClasses).iterator()
}
}