如何使用 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())
}
}
如何使用 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())
}
}