Свести тесты JUnit в классах-членах stati c - PullRequest
0 голосов
/ 17 февраля 2020

Для одного из моих проектов я использую JUnit 5 для тестирования кода отражения, который требует большого количества классов для тестовых случаев. Бросить их все в одну область и попытаться назвать их разумно, почти невозможно, поэтому я надеюсь поместить и методы тестирования, и типы, которые тестируются, в класс-член stati c. Это позволило бы мне повторно использовать имена, такие как X или Y, в каждом тесте и держать тестируемые типы рядом с кодом, который их тестирует. (Классы-члены должны быть stati c, чтобы я мог добавить интерфейсы)

Если я просто добавлю классы stati c, тесты будут работать из коробки, но в окончательном отчете я получу все классы-члены перечислены отдельно, поэтому я хотел бы иметь возможность объединить их все в один класс в отчете.

Вот пример того, чего я хотел бы достичь: (Я фактически пишем тесты в 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 в классах-членах, но это просто привело к дублированию имени в отчете. До сих пор я думаю, что, возможно, захочу использовать расширение, но после небольшого исследования я не нашел способа изменить класс, под которым тест перечислен в отчете, используя их.

Ответы [ 2 ]

0 голосов
/ 17 февраля 2020

После дополнительных копаний я смог достичь почти точно того, что хотел, используя dynamici c 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

Вот код, который делает это возможным. Он работает, сканируя все объявленные классы на наличие аннотированных методов и собирая все, которые аннотируются сами, а затем создает для них динамические c тесты, удостоверяясь, что указывает их исходные URI . Это в 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()
    }
}
0 голосов
/ 17 февраля 2020

Может быть, вы можете реорганизовать выходные файлы и индексы или дурачиться с ними, используя xsl / xslt или какую-либо другую форму постобработки. Кроме того, эта статья может представлять интерес.

...