【问题标题】:Flatten JUnit tests in static member classes在静态成员类中展平 JUnit 测试
【发布时间】:2020-02-16 23:59:30
【问题描述】:

对于我的一个项目,我使用 JUnit 5 来​​测试反射代码,这需要大量的类用于测试用例。将它们全部放在一个范围内并尝试智能地命名它们几乎是不可能的,因此我希望将测试方法和正在测试的类型都放在静态成员类中。这样做可以让我在每个测试中重用诸如XY 之类的名称,并将被测试的类型保持在测试它们的代码附近。 (成员类必须是静态的,所以我可以添加接口)

如果我只是添加静态类,测试运行良好开箱即用,但在最终报告中,我最终会单独列出所有成员类,所以我希望能够将它们全部“扁平化”到报告中的单个类。

这是我想要实现的示例:(我实际上是在 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

我尝试在成员类上使用@DisplayName,但它只是导致报告中出现重复名称。到目前为止,我想我可能想使用一个扩展,但是在做了一些研究之后,我还没有找到任何方法来改变使用它们的报告中列出的测试类。

【问题讨论】:

  • '静态内部是一个矛盾的术语。见JLS 8.1.3
  • @user207421 正确,我只是认为对于大多数回答这个问题的人来说,“内部阶层”会更熟悉。我修复了它,但为了任何人搜索这个问题,我恢复了它。我认为“内部阶层”只是一个更常见的搜索词。
  • 你还没有到处修复它。不要滥用标准术语。

标签: java junit junit5


【解决方案1】:

也许您可以重新组织输出文件和索引,或者使用 xsl/xslt 或其他形式的后期处理来处理它们。另外,这个article 可能很有趣。

【讨论】:

    【解决方案2】:

    经过更多的挖掘,我几乎可以使用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()
        }
    }
    

    【讨论】:

      最近更新 更多