Как сохранить значения в расширениях JUnit 5 и ввести их в параметризованный тест - PullRequest
0 голосов
/ 28 октября 2019

Обзор

Ожидается - Создайте класс JUnit 5 Extension для управления использованием TestCoroutineDispatcher.

Наблюдаемый - Невозможно получить доступ к переменной testDispatcher, созданной в классе Extension.

Реализация расширения

Test.kt


@ExtendWith(InstantExecutorExtension::class, MainCoroutineExtension::class)
class FeedLoadContentTests {
    private val contentViewModel = ContentViewModel()
    private fun FeedLoad() = feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    @ExtendWith(MainCoroutineExtension::class)
    fun `Feed Load`(test: FeedLoadContentTest) = testDispatcher.runBlockingTest {
        // Some testing done here.
    }
}

Extension.kt

class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback {
    val testDispatcher = TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

Ответы [ 2 ]

1 голос
/ 28 октября 2019

Не используйте переменные-члены для хранения и получения значений в расширениях Юпитера. Вместо этого используйте механизм хранения контекста расширения: https://junit.org/junit5/docs/5.5.1/api/org/junit/jupiter/api/extension/ExtensionContext.Store.html

Почему это так сложно? Причина в том, что жизненный цикл значения, которое вы хотите сохранить, и сам объект расширения в большинстве случаев не совпадают.

1 голос
/ 28 октября 2019

Вот три реализации, которые работают в теории. Однако последнее решение является лучшим, Храните значения расширения с getStore и параметры ввода с использованием ParameterResolver, поскольку оно обеспечивает безопасность жизненного цикла.

Спасибо @ johanneslink , за то, что направили меня в правильном направлении!

Регистрация расширения программы

Стратегия

TLDR - Использовать Регистрация программных расширений .

Эта стратегия работает, как и ожидалось, с TestCoroutineDispatcher, созданным в MainCoroutineExtension, и его жизненный цикл управляется с помощью тестовых реализаций жизненного цикла.

Реализация

Test.kt

class FeedLoadContentTests {

    companion object {
        @JvmField
        @RegisterExtension
        val mainCoroutineExtension = MainCoroutineExtension()
    }

    private val contentViewModel = ContentViewModel()
    private fun FeedLoad() = feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    @ExtendWith(MainCoroutineExtension::class)
    fun `Feed Load`(test: FeedLoadContentTest) = 
        mainCoroutineExtension.testDispatcher.runBlockingTest {
        // Some testing done here.
        }
}

Extension.kt

class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback {
    val testDispatcher = TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

InjectПараметры, использующие ParameterResolver

Стратегия

TLDR - Используйте ParameterResolver.

Этот подход реализует ParameterResolver для ввода TestCoroutineDispatcher, необходимого для управления жизненным циклом Coroutine в локальном тесте JUnit.

Реализация

Test.kt

@ExtendWith(LifecycleExtensions::class)
// The TestCoroutineDispatcher is injected here as a parameter.
class FeedLoadContentTests(val testDispatcher: TestCoroutineDispatcher) {

    private val contentViewModel = ContentViewModel()
    private fun FeedLoad() = feedLoadTestCases()

    @ParameterizedTest
    @MethodSource("FeedLoad")
    fun `Feed Load`(test: FeedLoadContentTest) = testDispatcher.runBlockingTest {
        // Some testing done here.
    }
}

Extension.kt

class LifecycleExtensions : = BeforeEachCallback, AfterEachCallback, ParameterResolver {

    val testDispatcher = TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(testDispatcher)
        ...
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset Coroutine Dispatcher.
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
        ...
    }

    override fun supportsParameter(parameterContext: ParameterContext?,
                                   extensionContext: ExtensionContext?) =
            parameterContext?.parameter?.type == TestCoroutineDispatcher::class.java

    override fun resolveParameter(parameterContext: ParameterContext?,
                                  extensionContext: ExtensionContext?) =
            testDispatcher

}

Сохранение значений расширения с getStore и параметров ввода с использованием ParameterResolver

Единственный рефактор, который отличается от Параметры ввода, использующие ParameterResolver выше, используют getStore для хранения TestCoroutineDispatcher. Важно, чтобы context?.root использовалось во избежание создания нескольких экземпляров введенного значения для одного класса Test.

Это вместо сохранения TestCoroutineDispatcher в качестве переменной-члена, что может привести к проблемам жизненного цикла, когдапараллельные тесты

Extension.kt

class LifecycleExtensions : BeforeAllCallback, AfterAllCallback, BeforeEachCallback,
        AfterEachCallback, ParameterResolver {
    ...

    override fun beforeEach(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(context?.root
                ?.getStore(STORE_NAMESPACE)
                ?.get(STORE_KEY, TestCoroutineDispatcher::class.java)!!)

        ...
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset Coroutine Dispatcher.
        Dispatchers.resetMain()
        context?.root
                ?.getStore(STORE_NAMESPACE)
                ?.get(STORE_KEY, TestCoroutineDispatcher::class.java)!!.cleanupTestCoroutines()

        ...
    }

    override fun supportsParameter(parameterContext: ParameterContext?,
                                   extensionContext: ExtensionContext?) =
            parameterContext?.parameter?.type == TestCoroutineDispatcher::class.java

    override fun resolveParameter(parameterContext: ParameterContext?,
                              extensionContext: ExtensionContext?) =
        getTestCoroutineDispatcher(extensionContext).let { dipatcher ->
            if (dipatcher == null) saveAndReturnTestCoroutineDispatcher(extensionContext)
            else dipatcher
        }

    private fun getTestCoroutineDispatcher(context: ExtensionContext?) = context?.root
        ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
        ?.get(TEST_COROUTINE_DISPATCHER_KEY, TestCoroutineDispatcher::class.java)

    private fun saveAndReturnTestCoroutineDispatcher(extensionContext: ExtensionContext?) =
        TestCoroutineDispatcher().apply {
            extensionContext?.root
                    ?.getStore(TEST_COROUTINE_DISPATCHER_NAMESPACE)
                    ?.put(TEST_COROUTINE_DISPATCHER_KEY, this)
        }
...