如何在 JUnit 5 扩展中存储值并在参数化测试中注入

How to Store Values in JUnit 5 Extensions and Inject in Parameterized Test

概述

预期 - 创建 JUnit 5 Extension class 以管理 TestCoroutineDispatcher.

的使用

Observed - 无法访问在 Extension class.

中创建的 testDispatcher 变量

扩展实现

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()
    }
}

以下是理论上可行的三种实现方式。然而,最后一个解决方案是最好的,Store Extension Values with getStore and Inject Parameters using ParameterResolver,因为它确保了生命周期安全。

感谢@johanneslink,指导我正确的方向!

程序化扩展注册

策略

TLDR - 使用 Programmatic Extension Registration.

此策略与在 MainCoroutineExtension 中创建的 TestCoroutineDispatcher 一起正常工作,其生命周期通过测试生命周期实施进行管理。

实施

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()
    }
}

使用ParameterResolver

注入参数

策略

TLDR - 使用 ParameterResolver.

此方法实现 ParameterResolver 以注入 TestCoroutineDispatcher 在本地 JUnit 测试中管理协程生命周期所需的 TestCoroutineDispatcher

实施

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 以避免为每个测试 class.

创建注入值的多个实例

这不是将 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)
        }

不要使用成员变量来存储和检索 Jupiter 扩展中的值。而是使用扩展上下文的存储机制:https://junit.org/junit5/docs/5.5.1/api/org/junit/jupiter/api/extension/ExtensionContext.Store.html

为什么这么复杂?原因是你要存储的值的生命周期和扩展对象本身在大多数情况下不匹配。