Kotlin Runtest with delay()不起作用
我正在测试一个阻塞的Coroutine。这是我的生产代码:
interface Incrementer {
fun inc()
}
class MyViewModel : Incrementer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val _number = MutableStateFlow(0)
fun getNumber(): StateFlow<Int> = _number.asStateFlow()
override fun inc() {
launch(coroutineContext) {
delay(100)
_number.tryEmit(1)
}
}
}
我的测试:
class IncTest {
@BeforeEach
fun setup() {
Dispatchers.setMain(StandardTestDispatcher())
}
@AfterEach
fun teardown() {
Dispatchers.resetMain()
}
@Test
fun incrementOnce() = runTest {
val viewModel = MyViewModel()
val results = mutableListOf<Int>()
val resultJob = viewModel.getNumber()
.onEach(results::add)
.launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
launch(StandardTestDispatcher(testScheduler)) {
viewModel.inc()
}.join()
assertEquals(listOf(0, 1), results)
resultJob.cancel()
}
}
如何测试我的 inc()功能? (将界面刻在石头上,所以我不能将 inc()变成悬浮函数。)
I am testing a coroutine that blocks. Here is my production code:
interface Incrementer {
fun inc()
}
class MyViewModel : Incrementer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val _number = MutableStateFlow(0)
fun getNumber(): StateFlow<Int> = _number.asStateFlow()
override fun inc() {
launch(coroutineContext) {
delay(100)
_number.tryEmit(1)
}
}
}
And my test:
class IncTest {
@BeforeEach
fun setup() {
Dispatchers.setMain(StandardTestDispatcher())
}
@AfterEach
fun teardown() {
Dispatchers.resetMain()
}
@Test
fun incrementOnce() = runTest {
val viewModel = MyViewModel()
val results = mutableListOf<Int>()
val resultJob = viewModel.getNumber()
.onEach(results::add)
.launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
launch(StandardTestDispatcher(testScheduler)) {
viewModel.inc()
}.join()
assertEquals(listOf(0, 1), results)
resultJob.cancel()
}
}
How would I go about testing my inc() function? (The interface is carved in stone, so I can't turn inc() into a suspend function.)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
这里有两个问题:
viewModel.inc()
在内部启动的工作。让我们从问题2开始:为此,您需要能够修改
myViewModel
(但不是inc
),然后更改类,以便不使用硬编码dispatchers.io
,它接收coroutinecontext
作为参数。这样,您可以在测试中传递testDisPatcher
,这将利用虚拟时间快速向前延迟。您可以在“ noreferrer”>注射testdispatchers Android文档。在这里,我还完成了一些较小的清理:
myViewModel
包含coroutinesCope
而不是实现接口,即正式推荐的练习coroutinecontext
参数传递给pluench> punains
, '在这种情况下做任何事情 - 无论如何,相同的上下文都在范围中,因此它已经用于问题1,等待工作完成,您有一些选择:
如果您通过了
testDisPatcher
,您可以使用Advanceununtilidle
之类的测试方法手动手动推进inc
内部创建的coroutine。这不是理想的选择,因为您非常依赖实施细节,这是您在生产中无法做的事情。但是,如果您无法使用下面的更好的解决方案,它将起作用。适当的解决方案将用于
inc
,可以让其呼叫者何时完成工作。您可以使其成为一种悬浮方法,而不是内部启动新的Coroutine,但您表示无法修改使其暂停的方法。替代方案(如果您能够进行此更改)是使用inc
使用async
构建器创建新的Coroutine,返回延迟
创建的对象,然后在呼叫网站上-ing。
如果您无法修改方法或类,则无法避免
delay> delay()
调用导致真正的100ms延迟。在这种情况下,您可以强迫测试在继续之前等待此时间。runtest
内的常规delay>
将通过使用testDisPatcher
为其创建的coroutine快速抛光,但您可以逃脱其中一种解决方案:对于有关测试代码的一些最终注释:
dispatchers.setmain
,因此您无需将testschedscheduler
传递到<<代码> testDisPatchers 您创建。他们将从main
中获取调度程序,如果他们在其中找到testDisPatcher
,如上所述在其文档中。this
,而不是创建新的范围以传递到lainingin
,它是runtest
的接收器,它指向a <代码> TestScope 。There are two problems here:
viewModel.inc()
launches internally.Let's start with problem #2 first: for this, you need to be able to modify
MyViewModel
(but notinc
), and change the class so that instead of using a hardcodedDispatchers.IO
, it receives aCoroutineContext
as a parameter. With this, you could pass in aTestDispatcher
in tests, which would use virtual time to fast-forward the delay. You can see this pattern described in the Injecting TestDispatchers section of the Android docs.Here, I've also done some minor cleanup:
MyViewModel
contain aCoroutineScope
instead of implementing the interface, which is an officially recommended practicecoroutineContext
parameter passed tolaunch
, as it doesn't do anything in this case - the same context is in the scope anyway, so it'll already be usedFor problem #1, waiting for work to complete, you have a few options:
If you've passed in a
TestDispatcher
, you can manually advance the coroutine created insideinc
using testing methods likeadvanceUntilIdle
. This is not ideal, because you're relying on implementation details a lot, and it's something you couldn't do in production. But it'll work if you can't use the nicer solution below.The proper solution would be for
inc
to let its callers know when it's done performing its work. You could make it a suspending method instead of launching a new coroutine internally, but you stated that you can't modify the method to make it suspending. An alternative - if you're able to make this change - would be to create the new coroutine ininc
using theasync
builder, returning theDeferred
object that that creates, and thenawait()
-ing at the call site.If you're not able to modify either the method or the class, there's no way to avoid the
delay()
call causing a real 100ms delay. In this case, you can force your test to wait for that amount of time before proceeding. A regulardelay()
withinrunTest
would be fast-forwarded thanks to it using aTestDispatcher
for the coroutine it creates, but you can get away with one of these solutions:For some final notes about the test code:
Dispatchers.setMain
, you don't need to pass intestScheduler
into theTestDispatchers
you create. They'll grab the scheduler fromMain
automatically if they find aTestDispatcher
there, as described in its docs.launchIn
, you could simply pass inthis
, the receiver ofrunTest
, which points to aTestScope
.