我想为我的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阻塞的协程调度程序?
答案 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,它也将不起作用。但这是我最好的解决方案。