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



概述

预期-创建一个JUnit 5Extension类以管理TestCoroutineDispatcher的使用。

观察-无法访问在Extension类中创建的testDispatcher变量。

扩展实施

测试.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.
}
}

扩展.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()
}
}

这里有三个理论上有效的实现。但是,最后一种解决方案是最好的,使用getStore存储扩展值,使用ParameterResolver注入参数,因为它可以确保生命周期安全。

感谢@johanneslink为我指引了正确的方向

程序扩展注册

战略

TLDR-使用程序扩展注册

该策略在MainCoroutineExtension中创建的TestCoroutineDispatcher及其"使用测试生命周期实现管理的生命周期"中按预期工作。

实施

测试.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.
}
}

扩展.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,以便在本地JUnit测试中注入管理Coroutine生命周期所需的TestCoroutineDispatcher

实施

测试.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.
}
}

扩展.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。为了避免为每个Test类创建多个注入值实例,使用context?.root是很重要的。

这不是将TestCoroutineDispatcher存储为成员变量,这可能会在并行运行测试时导致生命周期问题。

扩展.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

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

相关内容

  • 没有找到相关文章

最新更新