How to wait for suspendCoroutine in unit test?
我想为我的android应用程序编写测试。有时,viewModel使用Kotlins协程启动功能在后台执行任务。这些任务在androidx.lifecycle库方便提供的viewModelScope中执行。为了仍然测试这些功能,我用Dispatchers.Unconfined替换了默认的android Dispatchers,它可以同步运行代码。
至少在大多数情况下。当使用suspendCoroutine时,Dispatchers.Unconfined将不会被挂起并随后恢复,而只会返回。
[Dispatchers.Unconfined] lets the coroutine resume in whatever thread that is used by the corresponding suspending function.
因此,据我了解,协程实际上并未暂停,而是
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class TestClassTest { var testInstance = TestClass() @Test fun `Test with default dispatchers should fail`() { testInstance.runAsync() assertFalse(testInstance.valueToModify) } @Test fun `Test with dispatchers replaced by Unconfined should pass`() { testInstance.DefaultDispatcher = Dispatchers.Unconfined testInstance.IODispatcher = Dispatchers.Unconfined testInstance.runAsync() assertTrue(testInstance.valueToModify) } @Test fun `I need to also test some functions that use suspend coroutine - How can I do that?`() { testInstance.DefaultDispatcher = Dispatchers.Unconfined testInstance.IODispatcher = Dispatchers.Unconfined testInstance.runSuspendCoroutine() assertTrue(testInstance.valueToModify)//fails } } class TestClass { var DefaultDispatcher: CoroutineContext = Dispatchers.Default var IODispatcher: CoroutineContext = Dispatchers.IO val viewModelScope = CoroutineScope(DefaultDispatcher) var valueToModify = false fun runAsync() { viewModelScope.launch(DefaultDispatcher) { valueToModify = withContext(IODispatcher) { sleep(1000) true } } } fun runSuspendCoroutine() { viewModelScope.launch(DefaultDispatcher) { valueToModify = suspendCoroutine { Thread { sleep(1000) //long running operation calls listener from background thread it.resume(true) }.start() } } } } |
我正在尝试使用
我的问题:
我可以使用什么协程分派器来运行launch和withContext等阻塞(如
将协程上下文传递到模型怎么样?就像是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Model(parentContext: CoroutineContext = Dispatchers.Default) { private val modelScope = CoroutineScope(parentContext) var result = false fun doWork() { modelScope.launch { Thread.sleep(3000) result = true } } } @Test fun test() { val model = runBlocking { val model = Model(coroutineContext) model.doWork() model } println(model.result) } |
更新资料
对于来自androidx.lifecycle的viewModelScope,您可以只使用此测试规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @ExperimentalCoroutinesApi class CoroutinesTestRule : TestWatcher() { private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } } |
这是测试模型和测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class MainViewModel : ViewModel() { var result = false fun doWork() { viewModelScope.launch { Thread.sleep(3000) result = true } } } class MainViewModelTest { @ExperimentalCoroutinesApi @get:Rule var coroutinesRule = CoroutinesTestRule() private val model = MainViewModel() @Test fun `check result`() { model.doWork() assertTrue(model.result) } } |
不要忘记添加
据我了解,您希望生产代码中的所有内容都可以在测试的主线程中运行。但这似乎是无法实现的,因为可能会在常规线程池/后台运行某些内容,并且如果无法同步/加入代码中的后台进程,则可能会遇到麻烦。最好的办法是在断言之前在测试中以某种方式将协程和后台线程加入协程。可能需要您更改现有的生产代码。
我说的是另一种编写代码的方法。当然,这并不总是可能的。我已经相应地更新了您的示例以说明我的观点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class TestClassTest { var testInstance = TestClass() @Test fun `Test with default dispatchers should fail`() = runBlocking { val valueToModify = testInstance.runAsync().await() assertTrue(valueToModify) } @Test fun `Test with dispatchers replaced by Unconfined should pass`() = runBlocking { testInstance.DefaultDispatcher = Dispatchers.Unconfined testInstance.IODispatcher = Dispatchers.Unconfined val valueToModify = testInstance.runAsync().await() assertTrue(valueToModify) } @Test fun `I need to also test some functions that use suspend coroutine - How can I do that?`() = runBlocking { testInstance.DefaultDispatcher = Dispatchers.Unconfined testInstance.IODispatcher = Dispatchers.Unconfined val valueToModify = testInstance.runSuspendCoroutine().await() assertTrue(valueToModify)//fails } } class TestClass { var DefaultDispatcher: CoroutineContext = Dispatchers.Default var IODispatcher: CoroutineContext = Dispatchers.IO val viewModelScope = CoroutineScope(DefaultDispatcher) fun runAsync(): Deferred<Boolean> { return viewModelScope.async(DefaultDispatcher) { withContext(IODispatcher) { sleep(1000) true } } } fun runSuspendCoroutine(): Deferred<Boolean> { return viewModelScope.async(DefaultDispatcher) { suspendCoroutine<Boolean> { Thread { sleep(1000) //long running operation calls listener from background thread it.resume(true) }.start() } } } } |
在其他答案的帮助下,我找到了以下解决方案。正如sedovav在他的回答中建议的那样,我可以异步运行任务并等待Deferred。基本上是相同的想法,我可以通过启动运行任务,并等待处理任务的作业。两种情况下的问题是,如何获得延期或工作。
我找到了解决方案。这样的Job始终是CoroutineContext中包含的CoroutineContext中包含的Job的子级。因此,无论是在我的示例代码还是在实际的android应用程序中,以下代码都可以解决问题。
1 2 3 4 5 6 7 8 9 | @Test fun `I need to also test some functions that use suspend coroutine - How can I do that?`() { replaceDispatchers() testInstance.runSuspendCoroutine() runBlocking { (testInstance.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() } } assertTrue(testInstance.valueToModify)//fails } |
看起来确实有点像骇客,如果有人有理由这样做很危险,请告诉。如果由其他类创建的另一个基础CoroutineScope也将不起作用。但这是我最好的解决方案。
这正是创建
1 2 3 4 5 6 7 8 9 | result = runBlocking { suspendCoroutine { continuation -> Thread { sleep(1000) //long running operation calls listener from background thread continuation.resume(true) }.start() } } |