如何使用 ParameterResolver 在 JUnit 5 中注入多个扩展值

How to Inject Multiple Extension Values in JUnit 5 with ParameterResolver

如何使用 ParameterResolver 模式将多个值注入到测试中?

好像只能定义一个return值。

目前,getStore 保存一个扩展值,使用 ParameterResolver 作为参数注入。在这个例子中,它是一个 TestCoroutineDispatcher 注入来管理本地 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 : BeforeAllCallback, AfterAllCallback, BeforeEachCallback,
        AfterEachCallback, ParameterResolver {
    ...

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

        ...
    }

    override fun afterEach(context: ExtensionContext?) {
        // Reset Coroutine Dispatcher.
        Dispatchers.resetMain()
        context?.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?) =
            TestCoroutineDispatcher().apply {
                extensionContext?.getStore(STORE_NAMESPACE)?.put(STORE_KEY, this)
            }
}

感谢您的洞察力 @Slaw

Well, both #supportsParameter and #resolveParameter is invoked for each parameter in the method. So you currently have a test to see if the parameter type is TestCoroutineDispatcher which means you can add a test for the shared ViewModel type. Then resolve the correct parameter based on the type. If you want to share the ViewModel across multiple tests then you can store a reference to it in the parent/root ExtensionContext.Store.

Test.kt

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

    private fun FeedLoad() = feedLoadTestCases()

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

Extension.kt

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

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

        // Set ViewModel
        context?.root
                ?.getStore(VIEWMODEL_NAMESPACE)
                ?.get(CONTENT_VIEWMODEL_KEY, ContentViewModel::class.java)!!

        // Set LiveData Executor.
        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
            override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
            override fun postToMainThread(runnable: Runnable) = runnable.run()
            override fun isMainThread(): Boolean = true
        })
    }

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

        // Clear LiveData Executor
        ArchTaskExecutor.getInstance().setDelegate(null)
    }

    override fun resolveParameter(parameterContext: ParameterContext?,
                                  extensionContext: ExtensionContext?) =
            if (parameterContext?.parameter?.type == TestCoroutineDispatcher::class.java)
                getTestCoroutineDispatcher(extensionContext).let { dipatcher ->
                    if (dipatcher == null) saveAndReturnTestCoroutineDispatcher(extensionContext)
                    else dipatcher
                }
            else getViewModel(extensionContext).let { viewModel ->
                if (viewModel == null) saveAndReturnContentViewModel(extensionContext)
                else viewModel
            }

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

    private fun getViewModel(context: ExtensionContext?) = context?.root
            ?.getStore(VIEWMODEL_NAMESPACE)
            ?.get(CONTENT_VIEWMODEL_KEY, ContentViewModel::class.java)

    private fun saveAndReturnContentViewModel(extensionContext: ExtensionContext?) =
            ContentViewModel().apply {
                extensionContext?.root
                        ?.getStore(VIEWMODEL_NAMESPACE)
                        ?.put(CONTENT_VIEWMODEL_KEY, ContentViewModel())
            }
}