如何在 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
为什么这么复杂?原因是你要存储的值的生命周期和扩展对象本身在大多数情况下不匹配。
概述
预期 - 创建 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
为什么这么复杂?原因是你要存储的值的生命周期和扩展对象本身在大多数情况下不匹配。