如何在单元测试中等待suspendCoroutine?

时间:2019-07-02 11:14:16

标签: unit-testing kotlin kotlin-coroutines

我想为我的android应用程序编写测试。有时,viewModel使用Kotlins协程启动功能在后台执行任务。这些任务在androidx.lifecycle库方便提供的viewModelScope中执行。为了仍然测试这些功能,我用Dispatchers.Unconfined替换了默认的android Dispatchers,它可以同步运行代码。

至少在大多数情况下。当使用suspendCoroutine时,Dispatchers.Unconfined不会被挂起,以后再恢复,而只会返回。 Dispatchers.Unconfined的文档揭示了原因:

  

[Dispatchers.Unconfined]允许协程在相应的挂起函数使用的任何线程中恢复。

因此,据我了解,协程实际上并未暂停,但是suspendCoroutine之后的其余异步函数在调用continuation.resume的线程上运行。因此测试失败。

示例:

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

我正在尝试使用runBlocking,但是只有在您使用CoroutineScope创建的runBlocking调用启动的情况下,它才有帮助。但是,由于我的代码是在CoroutineScope提供的viewModelScope上启动的,因此无法使用。如果可能的话,我宁愿不要在任何地方注入CoroutineScope,因为任何较低级别的类都可以(并且确实)像示例中那样创建自己的CoroutineScope,然后Test必须了解很多实现细节。范围的思想是每个类都可以控制取消其异步操作。例如,当viewModel销毁时,默认情况下会取消viewModelScope。

我的问题: 我可以使用哪种启动和启动withContext等阻塞(例如Dispatchers.Unconfined)并且还可以运行suspendCoroutine阻塞的协程调度程序?

4 个答案:

答案 0 :(得分:1)

据我了解,您希望生产代码中的所有内容都可以在测试的主线程中运行。但这似乎是无法实现的,因为可能会在常规线程池/后台中运行某些内容,并且如果无法在代码中同步/加入后台进程,则可能会遇到麻烦。最好的办法是在断言之前在测试中以某种方式在协程和try: # for Python3 from tkinter import * except ImportError: # for Python2 from Tkinter import * root = Tk() root.geometry('430x480+50+50') root.title("Transparency Test") root["bg"] = "black" layer = PhotoImage(file ="rpm-overlay-overlay.png") topFrame = Label(text="Ping Checker", bg="black", image=layer, fg="#fff", font="Bahnschrift 14") topFrame.place(x=11,y=10) topFrame.pack_forget() topFrame.pack() root.wm_attributes('-transparentcolor','black') root.mainloop() 中加入后台线程。可能需要您更改现有的生产代码。

我说的是另一种编写代码的方法。当然,这并不总是可能的。我已经相应地更新了您的示例以说明我的观点:

await

答案 1 :(得分:1)

如何将协程上下文传递给模型?像

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,您可以使用此测试规则

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

这是测试模型和测试

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

不要忘记添加testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"以获得TestCoroutineDispatcher

答案 2 :(得分:0)

这正是创建runBlocking要做的。只需替换GlobalScope.launch调用:

result = runBlocking {
    suspendCoroutine { continuation ->
        Thread {
            sleep(1000)
            //long running operation calls listener from background thread
            continuation.resume(true)
        }.start()
    }
}

答案 3 :(得分:0)

借助其他答案,我找到了以下解决方案。正如sedovav在他的回答中建议的那样,我可以异步运行任务并等待Deferred。基本上是相同的想法,我可以通过启动运行任务,并等待处理任务的作业。两种情况下的问题都是,如何获得延期或工作。

我找到了解决方案。此类Job始终是CoroutineContext中包含的CoroutineContext中包含的Job的子级。因此,无论是在我的示例代码还是在实际的android应用程序中,以下代码都可以解决问题。

@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,它也将不起作用。但这是我最好的解决方案。

相关问题